mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
feat: ui store & updated nav
This commit is contained in:
parent
e1fd995d53
commit
f48c1d2a2c
11
src/App.tsx
11
src/App.tsx
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Switch } from 'react-router-dom';
|
import { Router, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Components
|
* Components
|
||||||
@ -14,17 +14,22 @@ import MainPage from 'pages/main';
|
|||||||
import CoursesPage from 'pages/courses';
|
import CoursesPage from 'pages/courses';
|
||||||
import MessagesPage from 'pages/messages';
|
import MessagesPage from 'pages/messages';
|
||||||
import ProfilePage from 'pages/profile';
|
import ProfilePage from 'pages/profile';
|
||||||
|
import ControlPage from 'pages/control';
|
||||||
import AuthPage from 'pages/auth';
|
import AuthPage from 'pages/auth';
|
||||||
import NotFoundPage from 'pages/404';
|
import NotFoundPage from 'pages/404';
|
||||||
|
import { UserRoleEnum } from 'enums';
|
||||||
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
const { uiStore } = useRootStore();
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router history={uiStore.history}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<PrivateRoute exact path='/' component={MainPage} title='Main' />
|
<PrivateRoute exact path='/' component={MainPage} title='Home' />
|
||||||
<PrivateRoute path='/courses' component={CoursesPage} title='Courses' />
|
<PrivateRoute path='/courses' component={CoursesPage} title='Courses' />
|
||||||
<PrivateRoute path='/messages' component={MessagesPage} title='Messages' />
|
<PrivateRoute path='/messages' component={MessagesPage} title='Messages' />
|
||||||
<PrivateRoute path='/profile' component={ProfilePage} title='Profile' />
|
<PrivateRoute path='/profile' component={ProfilePage} title='Profile' />
|
||||||
|
<PrivateRoute path='/control' component={ControlPage} title='Control' roles={[UserRoleEnum.ADMIN]} />
|
||||||
<PublicRoute path='/auth' component={AuthPage} />
|
<PublicRoute path='/auth' component={AuthPage} />
|
||||||
<PublicRoute component={NotFoundPage} />
|
<PublicRoute component={NotFoundPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import { MdAccountCircle, MdVpnKey } from 'react-icons/md';
|
import { MdAccountCircle, MdVpnKey } from 'react-icons/md';
|
||||||
import { FormControl, FormLabel, InputGroup, Input, InputLeftElement, Button, Icon, useToast } from '@chakra-ui/react';
|
import { FormControl, FormLabel, InputGroup, Input, InputLeftElement, Button, Icon, useToast } from '@chakra-ui/react';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
@ -15,14 +14,12 @@ type AuthInputs = {
|
|||||||
const AuthForm: React.FC = () => {
|
const AuthForm: React.FC = () => {
|
||||||
const { register, reset, handleSubmit } = useForm<AuthInputs>();
|
const { register, reset, handleSubmit } = useForm<AuthInputs>();
|
||||||
const { authStore } = useRootStore();
|
const { authStore } = useRootStore();
|
||||||
const history = useHistory();
|
|
||||||
const handleError = useErrorHandler();
|
const handleError = useErrorHandler();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<AuthInputs> = async data => {
|
const onSubmit: SubmitHandler<AuthInputs> = async data => {
|
||||||
try {
|
try {
|
||||||
await authStore.login(data.login, data.password);
|
await authStore.login(data.login, data.password);
|
||||||
history.push('/');
|
|
||||||
toast({
|
toast({
|
||||||
title: `👋 Welcome back!`,
|
title: `👋 Welcome back!`,
|
||||||
description: 'You are successfully logged in.',
|
description: 'You are successfully logged in.',
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import UserInfo from './User/UserInfo';
|
|||||||
import UserNotifications from './User/UserNotifications';
|
import UserNotifications from './User/UserNotifications';
|
||||||
import useWindowDimensions from 'hooks/useWindowDimensions';
|
import useWindowDimensions from 'hooks/useWindowDimensions';
|
||||||
import NavMobile from '../Nav/Mobile/Nav';
|
import NavMobile from '../Nav/Mobile/Nav';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
const { isDesktop } = useWindowDimensions();
|
const { isDesktop } = useWindowDimensions();
|
||||||
@ -45,4 +46,4 @@ const Header: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default observer(Header);
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import { MdSettings, MdExitToApp } from 'react-icons/md';
|
|||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
import { useHistory, Link } from 'react-router-dom';
|
import { useHistory, Link } from 'react-router-dom';
|
||||||
import UserBadge from 'components/Header/User/UserBadge';
|
import UserBadge from 'components/User/UserBadge';
|
||||||
|
|
||||||
const UserInfo: React.FC = () => {
|
const UserInfo: React.FC = () => {
|
||||||
const { authStore, userStore } = useRootStore();
|
const { authStore, userStore } = useRootStore();
|
||||||
@ -36,7 +36,7 @@ const UserInfo: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (userStore.me === null) {
|
if (userStore.me === null) {
|
||||||
return <Skeleton width='260px' borderRadius='md' />;
|
return <Skeleton width='260px' height='40px' borderRadius='md' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,9 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from '@chakra-ui/layout';
|
import { Box } from '@chakra-ui/layout';
|
||||||
|
import { Skeleton } from '@chakra-ui/skeleton';
|
||||||
import { Button } from '@chakra-ui/button';
|
import { Button } from '@chakra-ui/button';
|
||||||
import { MdNotifications } from 'react-icons/md';
|
import { MdNotifications } from 'react-icons/md';
|
||||||
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
const UserNotifications: React.FC = () => {
|
const UserNotifications: React.FC = () => {
|
||||||
|
const { userStore } = useRootStore();
|
||||||
|
|
||||||
|
if (userStore.me === null) {
|
||||||
|
return <Skeleton width='50px' height='40px' mr={3} ml={3} borderRadius='md' />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
mr={3}
|
mr={3}
|
||||||
@ -30,4 +39,4 @@ const UserNotifications: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserNotifications;
|
export default observer(UserNotifications);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { observer } from 'mobx-react';
|
|||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
import { ColorModeSwitcher } from 'components/ColorModeSwitcher/ColorModeSwitcher';
|
import { ColorModeSwitcher } from 'components/ColorModeSwitcher/ColorModeSwitcher';
|
||||||
|
import LoadingPage from 'components/Loading/LoadingPage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -37,7 +38,7 @@ const Layout: React.FC = ({ children }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box as='main' mr='auto' ml='auto' maxW='85rem' pl={5} pr={5} pt='2rem' pb='2rem'>
|
<Box as='main' mr='auto' ml='auto' maxW='85rem' pl={5} pr={5} pt='2rem' pb='2rem'>
|
||||||
{children}
|
{userStore.me === null ? <LoadingPage /> : children}
|
||||||
<ColorModeSwitcher position='absolute' right={0} bottom={0} margin={5} />
|
<ColorModeSwitcher position='absolute' right={0} bottom={0} margin={5} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
14
src/components/Loading/LoadingPage.tsx
Normal file
14
src/components/Loading/LoadingPage.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Skeleton } from '@chakra-ui/skeleton';
|
||||||
|
import { Box } from '@chakra-ui/layout';
|
||||||
|
|
||||||
|
const LoadingPage: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Skeleton height='20px' width='200px' mb='10' borderRadius='md' />
|
||||||
|
<Skeleton height='43px' width='400px' borderRadius='md' />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingPage;
|
||||||
@ -1,8 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import NavLink from './NavLink';
|
|
||||||
import config from 'config';
|
|
||||||
import { Flex, Box } from '@chakra-ui/react';
|
import { Flex, Box } from '@chakra-ui/react';
|
||||||
import useWindowDimensions from 'hooks/useWindowDimensions';
|
import useWindowDimensions from 'hooks/useWindowDimensions';
|
||||||
|
import NavList from '../NavList';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -24,9 +23,7 @@ const Nav: React.FC = () => {
|
|||||||
height='calc(100vh - 4rem)'
|
height='calc(100vh - 4rem)'
|
||||||
padding='5'
|
padding='5'
|
||||||
>
|
>
|
||||||
{config.links.map((link, i) => (
|
<NavList />
|
||||||
<NavLink link={link} key={i} />
|
|
||||||
))}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import config from 'config';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
|
||||||
@ -9,7 +8,7 @@ import { Flex, Box, Text } from '@chakra-ui/layout';
|
|||||||
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/modal';
|
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/modal';
|
||||||
import { HamburgerIcon } from '@chakra-ui/icons';
|
import { HamburgerIcon } from '@chakra-ui/icons';
|
||||||
import { useDisclosure, IconButton, useColorMode, Skeleton } from '@chakra-ui/react';
|
import { useDisclosure, IconButton, useColorMode, Skeleton } from '@chakra-ui/react';
|
||||||
import NavLink from '../Desktop/NavLink';
|
import NavList from '../NavList';
|
||||||
|
|
||||||
const NavMobile: React.FC = () => {
|
const NavMobile: React.FC = () => {
|
||||||
const { userStore } = useRootStore();
|
const { userStore } = useRootStore();
|
||||||
@ -24,9 +23,7 @@ const NavMobile: React.FC = () => {
|
|||||||
<DrawerContent backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}>
|
<DrawerContent backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}>
|
||||||
<DrawerBody>
|
<DrawerBody>
|
||||||
<Flex as='nav' flexDirection='column' alignItems='flex-start' textDecoration='none'>
|
<Flex as='nav' flexDirection='column' alignItems='flex-start' textDecoration='none'>
|
||||||
{config.links.map((link, i) => (
|
<NavList onCloseDrawer={onClose} />
|
||||||
<NavLink link={link} key={i} onCloseDrawer={onClose} />
|
|
||||||
))}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
{userStore.me ? (
|
{userStore.me ? (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@ -2,18 +2,19 @@ import React from 'react';
|
|||||||
import { Box, useColorMode } from '@chakra-ui/react';
|
import { Box, useColorMode } from '@chakra-ui/react';
|
||||||
import type { LinkType } from 'interfaces';
|
import type { LinkType } from 'interfaces';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
|
||||||
type ActiveLinkState = {
|
export type NavLinkProps = {
|
||||||
title: string | null;
|
link: LinkType;
|
||||||
|
onCloseDrawer?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* NavLink component
|
|
||||||
* Returns the link component for navigation.
|
* Returns the link component for navigation.
|
||||||
*/
|
*/
|
||||||
const NavLink: React.FC<{ link: LinkType; onCloseDrawer?: () => void }> = ({ link, onCloseDrawer }) => {
|
const NavLink: React.FC<NavLinkProps> = ({ link, onCloseDrawer }) => {
|
||||||
const [activeLink, setActiveLink] = React.useState<ActiveLinkState>({ title: null });
|
const { uiStore } = useRootStore();
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@ -27,8 +28,6 @@ const NavLink: React.FC<{ link: LinkType; onCloseDrawer?: () => void }> = ({ lin
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as='a'
|
as='a'
|
||||||
onMouseEnter={() => setActiveLink({ title: link.title })}
|
|
||||||
onMouseLeave={() => setActiveLink({ title: null })}
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
display='flex'
|
display='flex'
|
||||||
margin='3px 0'
|
margin='3px 0'
|
||||||
@ -39,12 +38,8 @@ const NavLink: React.FC<{ link: LinkType; onCloseDrawer?: () => void }> = ({ lin
|
|||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
color={history.location.pathname === link.location || activeLink.title === link.title ? 'blue.400' : ''}
|
color={uiStore.location === link.location ? 'blue.400' : ''}
|
||||||
backgroundColor={
|
backgroundColor={uiStore.location === link.location ? `${colorMode === 'dark' ? 'gray.700' : 'gray.100'}` : ''}
|
||||||
activeLink.title === link.title || history.location.pathname === link.location
|
|
||||||
? `${colorMode === 'dark' ? 'gray.700' : 'gray.100'}`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
borderRadius='9999px'
|
borderRadius='9999px'
|
||||||
display='flex'
|
display='flex'
|
||||||
@ -59,4 +54,4 @@ const NavLink: React.FC<{ link: LinkType; onCloseDrawer?: () => void }> = ({ lin
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NavLink;
|
export default observer(NavLink);
|
||||||
34
src/components/Nav/NavList.tsx
Normal file
34
src/components/Nav/NavList.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import config from 'config';
|
||||||
|
import NavLink from './NavLink';
|
||||||
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
import { userHasRoles } from 'helpers';
|
||||||
|
import { UserRoleEnum } from 'enums';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a navigation list of links
|
||||||
|
*/
|
||||||
|
const NavList: React.FC<{ onCloseDrawer?: () => void }> = ({ onCloseDrawer }) => {
|
||||||
|
const { userStore } = useRootStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{config.links.map((link, i) => {
|
||||||
|
if (link.public) {
|
||||||
|
return <NavLink link={link} key={i} onCloseDrawer={onCloseDrawer} />;
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* Render not public link
|
||||||
|
*/
|
||||||
|
if (userStore.me && userHasRoles(userStore.me.roles, [UserRoleEnum.ADMIN])) {
|
||||||
|
return <NavLink link={link} key={i} onCloseDrawer={onCloseDrawer} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(NavList);
|
||||||
@ -1,18 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import Layout from 'components/Layout/Layout';
|
import Layout from '../Layout/Layout';
|
||||||
import { Route, Redirect } from 'react-router-dom';
|
import { Route, Redirect } from 'react-router-dom';
|
||||||
import { IPrivateRouteProps } from 'interfaces';
|
import { IPrivateRouteProps } from 'interfaces';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import ErrorFallback from '../ErrorFallback';
|
import ErrorFallback from '../ErrorFallback';
|
||||||
|
import config from 'config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the user is loggedIn by reactive variable and if not,
|
* Checks if the user is loggedIn by reactive variable and if not,
|
||||||
* then a redirect to "/auth" route.
|
* then a redirect to "/auth" route.
|
||||||
*/
|
*/
|
||||||
const PrivateRoute: React.FC<IPrivateRouteProps> = ({ component: Component, title, ...options }) => {
|
const PrivateRoute: React.FC<IPrivateRouteProps> = ({ component: Component, title, roles, ...options }) => {
|
||||||
const { authStore } = useRootStore();
|
const { authStore } = useRootStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -23,9 +24,11 @@ const PrivateRoute: React.FC<IPrivateRouteProps> = ({ component: Component, titl
|
|||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{title} • App </title>
|
<title>
|
||||||
|
{title} • {config.metaData.appName}{' '}
|
||||||
|
</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Component {...props} title={title} />
|
<Component {...props} title={title} roles={roles} />
|
||||||
</Layout>
|
</Layout>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { MdAccountCircle, MdCollectionsBookmark, MdHome, MdMessage } from 'react-icons/md';
|
import { MdAccountCircle, MdBuild, MdCollectionsBookmark, MdHome, MdMessage } from 'react-icons/md';
|
||||||
import { IAppConfig } from './interfaces';
|
import { IAppConfig } from './interfaces';
|
||||||
|
|
||||||
const config: IAppConfig = {
|
const config: IAppConfig = {
|
||||||
@ -7,10 +7,11 @@ const config: IAppConfig = {
|
|||||||
appDescription: 'Learn management system',
|
appDescription: 'Learn management system',
|
||||||
},
|
},
|
||||||
links: [
|
links: [
|
||||||
{ location: '/', title: 'Home', icon: MdHome },
|
{ location: '/', title: 'Home', public: true, icon: MdHome },
|
||||||
{ location: '/courses', title: 'Courses', icon: MdCollectionsBookmark },
|
{ location: '/courses', title: 'Courses', public: true, icon: MdCollectionsBookmark },
|
||||||
{ location: '/messages', title: 'Messages', icon: MdMessage },
|
{ location: '/messages', title: 'Messages', public: true, icon: MdMessage },
|
||||||
{ location: '/profile', title: 'Profile', icon: MdAccountCircle },
|
{ location: '/profile', title: 'Profile', public: true, icon: MdAccountCircle },
|
||||||
|
{ location: '/control', title: 'Control', public: false, icon: MdBuild },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
14
src/helpers/index.ts
Normal file
14
src/helpers/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { UserRoleEnum } from 'enums';
|
||||||
|
import { IUserRole } from 'interfaces';
|
||||||
|
|
||||||
|
export function userHasRoles(userRoles: IUserRole[], roles: UserRoleEnum[]): boolean {
|
||||||
|
for (let i = 0; i < roles.length; i += 1) {
|
||||||
|
if (!userRoles.map(r => r.slug).includes(roles[i])) {
|
||||||
|
/**
|
||||||
|
* Unable to find current role in user roles
|
||||||
|
*/
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import { UserRoleEnum } from '../enums';
|
|||||||
export type LinkType = {
|
export type LinkType = {
|
||||||
location: string;
|
location: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
public: boolean;
|
||||||
icon: IconType;
|
icon: IconType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -17,6 +18,14 @@ export interface IAppConfig {
|
|||||||
links: LinkType[];
|
links: LinkType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUser {
|
||||||
|
id: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
email: string;
|
||||||
|
roles: IUserRole[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IUserRole {
|
export interface IUserRole {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -49,11 +58,13 @@ export interface IUserResult extends IDataResult {
|
|||||||
|
|
||||||
export interface IPageProps extends RouteComponentProps {
|
export interface IPageProps extends RouteComponentProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
roles?: UserRoleEnum[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPrivateRouteProps extends RouteProps {
|
export interface IPrivateRouteProps extends RouteProps {
|
||||||
component: React.FC<IPageProps>;
|
component: React.FC<IPageProps>;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
roles?: UserRoleEnum[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPublicRouteProps extends RouteProps {
|
export interface IPublicRouteProps extends RouteProps {
|
||||||
|
|||||||
39
src/pages/control.tsx
Normal file
39
src/pages/control.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { IPageProps } from 'interfaces';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
import { userHasRoles } from 'helpers';
|
||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control page
|
||||||
|
*/
|
||||||
|
const ControlPage: React.FC<IPageProps> = ({ title, roles }) => {
|
||||||
|
const { userStore } = useRootStore();
|
||||||
|
|
||||||
|
if (userStore.me && roles && !userHasRoles(userStore.me.roles, roles)) {
|
||||||
|
return <Redirect to='/404' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
||||||
|
<BreadcrumbItem isCurrentPage>
|
||||||
|
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</Breadcrumb>
|
||||||
|
<Box mt='10'>
|
||||||
|
<Box>
|
||||||
|
<Heading as='h1'>Control page</Heading>
|
||||||
|
<Grid templateColumns='3fr 1fr' mt='25px'>
|
||||||
|
...
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(ControlPage);
|
||||||
@ -12,7 +12,7 @@ const MainPage: React.FC<IPageProps> = ({ title }) => {
|
|||||||
<>
|
<>
|
||||||
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
||||||
<BreadcrumbItem isCurrentPage>
|
<BreadcrumbItem isCurrentPage>
|
||||||
<BreadcrumbLink href='/'>Main page</BreadcrumbLink>
|
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<Box mt='10'>
|
<Box mt='10'>
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export default class AuthStore {
|
|||||||
const result = await this.authService.requestLogout();
|
const result = await this.authService.requestLogout();
|
||||||
|
|
||||||
this.setIsLoggedIn(false);
|
this.setIsLoggedIn(false);
|
||||||
|
this.root.userStore.reset();
|
||||||
Cookies.remove('logged_in');
|
Cookies.remove('logged_in');
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -11,8 +11,11 @@ import ApiService from 'services/ApiService';
|
|||||||
*/
|
*/
|
||||||
import AuthStore from './AuthStore';
|
import AuthStore from './AuthStore';
|
||||||
import UserStore from './UserStore';
|
import UserStore from './UserStore';
|
||||||
|
import UIStore from './UIStore';
|
||||||
|
|
||||||
export default class RootStore {
|
export default class RootStore {
|
||||||
|
public uiStore: UIStore;
|
||||||
|
|
||||||
public authStore: AuthStore;
|
public authStore: AuthStore;
|
||||||
|
|
||||||
public userStore: UserStore;
|
public userStore: UserStore;
|
||||||
@ -32,6 +35,7 @@ export default class RootStore {
|
|||||||
/**
|
/**
|
||||||
* Stores
|
* Stores
|
||||||
*/
|
*/
|
||||||
|
this.uiStore = new UIStore(this);
|
||||||
this.authStore = new AuthStore(this, this.apiService.api);
|
this.authStore = new AuthStore(this, this.apiService.api);
|
||||||
this.userStore = new UserStore(this, this.apiService.api);
|
this.userStore = new UserStore(this, this.apiService.api);
|
||||||
|
|
||||||
|
|||||||
27
src/stores/UIStore.ts
Normal file
27
src/stores/UIStore.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
import RootStore from './RootStore';
|
||||||
|
|
||||||
|
export default class UIStore {
|
||||||
|
public root: RootStore;
|
||||||
|
|
||||||
|
public location: string = document.location.pathname;
|
||||||
|
|
||||||
|
public history: ReturnType<typeof createBrowserHistory>;
|
||||||
|
|
||||||
|
constructor(root: RootStore) {
|
||||||
|
this.root = root;
|
||||||
|
this.history = createBrowserHistory();
|
||||||
|
makeAutoObservable(this);
|
||||||
|
|
||||||
|
this.history.listen(({ pathname }) => {
|
||||||
|
console.log('path chaged');
|
||||||
|
|
||||||
|
this.location = pathname;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLocation(location: string) {
|
||||||
|
this.location = location;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +1,10 @@
|
|||||||
import { AxiosInstance } from 'axios';
|
import { AxiosInstance } from 'axios';
|
||||||
import { makeAutoObservable, runInAction } from 'mobx';
|
import { makeAutoObservable, runInAction } from 'mobx';
|
||||||
import { IUserResult, IUserRole } from 'interfaces';
|
import { IUserResult, IUser } from 'interfaces';
|
||||||
|
|
||||||
import UserService from 'services/UserService';
|
import UserService from 'services/UserService';
|
||||||
import RootStore from './RootStore';
|
import RootStore from './RootStore';
|
||||||
|
|
||||||
export interface IUser {
|
|
||||||
id: string;
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
email: string;
|
|
||||||
roles: IUserRole[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class UserStore {
|
export default class UserStore {
|
||||||
public root: RootStore;
|
public root: RootStore;
|
||||||
|
|
||||||
@ -41,4 +33,10 @@ export default class UserStore {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public reset(): void {
|
||||||
|
runInAction(() => {
|
||||||
|
this.me = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user