mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 02:09:08 +03:00
feat: added some pages & error handling on auth
This commit is contained in:
parent
8ef0d4f8f2
commit
2245c4aa85
10
.prettierrc.json
Normal file
10
.prettierrc.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "auto",
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -12527,6 +12527,14 @@
|
|||||||
"scheduler": "^0.20.2"
|
"scheduler": "^0.20.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-error-boundary": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-error-overlay": {
|
"react-error-overlay": {
|
||||||
"version": "6.0.9",
|
"version": "6.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"mobx-react": "^7.2.0",
|
"mobx-react": "^7.2.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-error-boundary": "^3.1.3",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-hook-form": "^7.12.2",
|
"react-hook-form": "^7.12.2",
|
||||||
"react-icons": "^3.11.0",
|
"react-icons": "^3.11.0",
|
||||||
|
|||||||
44
src/App.tsx
44
src/App.tsx
@ -1,42 +1,34 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
|
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
|
||||||
import PrivateRoute from './components/PrivateRoute/PrivateRoute';
|
import PrivateRoute from './components/PrivateRoute/PrivateRoute';
|
||||||
import { useRootStore } from './hooks/useRootStore';
|
import ErrorFallback from './components/ErrorFallback';
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pages
|
* Pages
|
||||||
*/
|
*/
|
||||||
import MainPage from './pages/main';
|
import MainPage from './pages/main';
|
||||||
|
import CoursesPage from './pages/courses';
|
||||||
|
import MessagesPage from './pages/messages';
|
||||||
|
import ProfilePage from './pages/profile';
|
||||||
import AuthPage from './pages/auth';
|
import AuthPage from './pages/auth';
|
||||||
import NotFoundPage from './pages/404';
|
import NotFoundPage from './pages/404';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { autorun } from 'mobx';
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const rootStore = useRootStore();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
autorun(() => {
|
|
||||||
if (rootStore.networkApiError !== undefined) {
|
|
||||||
toast({
|
|
||||||
title: `Network error while connecting to the server.`,
|
|
||||||
description: 'Try your request later.',
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<Switch>
|
<Router>
|
||||||
<PrivateRoute exact path='/' component={MainPage} title='Main' />
|
<Switch>
|
||||||
<Route path='/auth' component={AuthPage} />
|
<PrivateRoute exact path='/' component={MainPage} title='Main' />
|
||||||
<Route component={NotFoundPage} />
|
<PrivateRoute path='/courses' component={CoursesPage} title='Courses' />
|
||||||
</Switch>
|
<PrivateRoute path='/messages' component={MessagesPage} title='Messages' />
|
||||||
</Router>
|
<PrivateRoute path='/profile' component={ProfilePage} title='Profile' />
|
||||||
|
<Route path='/auth' component={AuthPage} />
|
||||||
|
<Route component={NotFoundPage} />
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MdAccountCircle, MdVpnKey } from 'react-icons/md';
|
import { MdAccountCircle, MdVpnKey } from 'react-icons/md';
|
||||||
import {
|
import { FormControl, FormLabel, InputGroup, Input, InputLeftElement, Button, Icon, useToast } from '@chakra-ui/react';
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
InputGroup,
|
|
||||||
Input,
|
|
||||||
InputLeftElement,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useRootStore } from '../../hooks/useRootStore';
|
import { useRootStore } from '../../hooks/useRootStore';
|
||||||
import { useHistory } from 'react-router-dom';
|
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';
|
||||||
|
|
||||||
type AuthInputs = {
|
type AuthInputs = {
|
||||||
login: string;
|
login: string;
|
||||||
@ -23,13 +16,35 @@ 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 history = useHistory();
|
||||||
|
const handleError = useErrorHandler();
|
||||||
|
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('/');
|
history.push('/');
|
||||||
|
toast({
|
||||||
|
title: `👋 Welcome back!`,
|
||||||
|
description: 'You are successfully logged in.',
|
||||||
|
isClosable: true,
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error while log in ');
|
if (error.response) {
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 404:
|
||||||
|
toast({ title: 'User not found in a system.', status: 'error' });
|
||||||
|
break;
|
||||||
|
case 401:
|
||||||
|
toast({ title: 'Invalid password.', status: 'error' });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
handleError(error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
@ -41,22 +56,14 @@ const AuthForm: React.FC = () => {
|
|||||||
<FormLabel>Login</FormLabel>
|
<FormLabel>Login</FormLabel>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputLeftElement children={<Icon as={MdAccountCircle} />} />
|
<InputLeftElement children={<Icon as={MdAccountCircle} />} />
|
||||||
<Input
|
<Input type='text' placeholder='Type your login' {...register('login', { required: true })} />
|
||||||
type='text'
|
|
||||||
placeholder='email@mail.com'
|
|
||||||
{...register('login', { required: true })}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl mt={5} isRequired={true}>
|
<FormControl mt={5} isRequired={true}>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputLeftElement children={<Icon as={MdVpnKey} />} />
|
<InputLeftElement children={<Icon as={MdVpnKey} />} />
|
||||||
<Input
|
<Input type='password' placeholder='********' {...register('password', { required: true })} />
|
||||||
type='password'
|
|
||||||
placeholder='********'
|
|
||||||
{...register('password', { required: true })}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Button
|
<Button
|
||||||
@ -66,7 +73,8 @@ const AuthForm: React.FC = () => {
|
|||||||
colorScheme='blue'
|
colorScheme='blue'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
width='full'
|
width='full'
|
||||||
mt={4}>
|
mt={4}
|
||||||
|
>
|
||||||
Log in
|
Log in
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,26 +1,21 @@
|
|||||||
import * as React from "react";
|
import * as React from 'react';
|
||||||
import {
|
import { useColorMode, useColorModeValue, IconButton, IconButtonProps } from '@chakra-ui/react';
|
||||||
useColorMode,
|
import { FaMoon, FaSun } from 'react-icons/fa';
|
||||||
useColorModeValue,
|
|
||||||
IconButton,
|
|
||||||
IconButtonProps,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { FaMoon, FaSun } from "react-icons/fa";
|
|
||||||
|
|
||||||
type ColorModeSwitcherProps = Omit<IconButtonProps, "aria-label">;
|
type ColorModeSwitcherProps = Omit<IconButtonProps, 'aria-label'>;
|
||||||
|
|
||||||
export const ColorModeSwitcher: React.FC<ColorModeSwitcherProps> = (props) => {
|
export const ColorModeSwitcher: React.FC<ColorModeSwitcherProps> = props => {
|
||||||
const { toggleColorMode } = useColorMode();
|
const { toggleColorMode } = useColorMode();
|
||||||
const text = useColorModeValue("dark", "light");
|
const text = useColorModeValue('dark', 'light');
|
||||||
const SwitchIcon = useColorModeValue(FaMoon, FaSun);
|
const SwitchIcon = useColorModeValue(FaMoon, FaSun);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="md"
|
size='md'
|
||||||
fontSize="lg"
|
fontSize='lg'
|
||||||
variant="ghost"
|
variant='ghost'
|
||||||
color="current"
|
color='current'
|
||||||
marginLeft="2"
|
marginLeft='2'
|
||||||
onClick={toggleColorMode}
|
onClick={toggleColorMode}
|
||||||
icon={<SwitchIcon />}
|
icon={<SwitchIcon />}
|
||||||
aria-label={`Switch to ${text} mode`}
|
aria-label={`Switch to ${text} mode`}
|
||||||
|
|||||||
26
src/components/ErrorFallback.tsx
Normal file
26
src/components/ErrorFallback.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useToast, Flex, Box, Heading, Text } from '@chakra-ui/react';
|
||||||
|
import { MdError } from 'react-icons/md';
|
||||||
|
|
||||||
|
const ErrorFallback: React.FC<{ error: any }> = props => {
|
||||||
|
const { error } = props;
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
toast({ title: `${error.message}`, duration: 10000, isClosable: true, status: 'error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box textAlign='center' mt={40}>
|
||||||
|
<Flex justifyContent='center'>
|
||||||
|
<MdError size='64px' />
|
||||||
|
</Flex>
|
||||||
|
<Heading display='block' as='h1' fontSize='6xl'>
|
||||||
|
Something went wrong
|
||||||
|
</Heading>
|
||||||
|
<Text mt='5'>Sorry, something went wrong. Reload page and try your request again later.</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorFallback;
|
||||||
@ -16,14 +16,16 @@ const Header: React.FC = () => {
|
|||||||
borderBottomWidth={1}
|
borderBottomWidth={1}
|
||||||
width='100%'
|
width='100%'
|
||||||
height='4rem'
|
height='4rem'
|
||||||
zIndex={4}>
|
zIndex={4}
|
||||||
|
>
|
||||||
<Flex
|
<Flex
|
||||||
justifyContent='space-between'
|
justifyContent='space-between'
|
||||||
width='100%'
|
width='100%'
|
||||||
height='100%'
|
height='100%'
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
paddingLeft='1.5rem'
|
paddingLeft='1.5rem'
|
||||||
paddingRight='1.5rem'>
|
paddingRight='1.5rem'
|
||||||
|
>
|
||||||
<AppName />
|
<AppName />
|
||||||
<Flex>
|
<Flex>
|
||||||
<UserNotifications />
|
<UserNotifications />
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Text,
|
Text,
|
||||||
Link,
|
Link,
|
||||||
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { MdSettings, MdExitToApp } from 'react-icons/md';
|
import { MdSettings, MdExitToApp } from 'react-icons/md';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
@ -21,13 +22,13 @@ import { useHistory } from 'react-router-dom';
|
|||||||
const UserInfo: React.FC = () => {
|
const UserInfo: React.FC = () => {
|
||||||
const { authStore } = useRootStore();
|
const { authStore } = useRootStore();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
const onLogoutHandler = async (
|
const onLogoutHandler = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
await authStore.logout();
|
await authStore.logout();
|
||||||
history.push('/auth');
|
history.push('/auth');
|
||||||
|
toast({ title: 'You are logged out.', isClosable: true, status: 'info' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,8 @@ const UserNotifications: React.FC = () => {
|
|||||||
width: '15px',
|
width: '15px',
|
||||||
height: '15px',
|
height: '15px',
|
||||||
borderRadius: '100%',
|
borderRadius: '100%',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Box as={MdNotifications} boxSize='21px' />
|
<Box as={MdNotifications} boxSize='21px' />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, Box } from '@chakra-ui/react';
|
import { Flex, Box } from '@chakra-ui/react';
|
||||||
import Header from '../Header/Header';
|
import Header from '../Header/Header';
|
||||||
|
import Nav from '../Nav/Nav';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -11,17 +12,10 @@ const Layout: React.FC = ({ children }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
|
<Nav />
|
||||||
<Flex minH='100vh' flexDirection='column'>
|
<Flex minH='100vh' flexDirection='column'>
|
||||||
<Box pl='18rem' mt='4rem' mb='4rem' flex='1 0 auto'>
|
<Box pl='18rem' mt='4rem' mb='4rem' flex='1 0 auto'>
|
||||||
<Box
|
<Box as='main' mr='auto' ml='auto' maxW='85rem' pl={5} pr={5} pt='2rem' pb='2rem'>
|
||||||
as='main'
|
|
||||||
mr='auto'
|
|
||||||
ml='auto'
|
|
||||||
maxW='85rem'
|
|
||||||
pl={5}
|
|
||||||
pr={5}
|
|
||||||
pt='2rem'
|
|
||||||
pb='2rem'>
|
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
52
src/components/Nav/Nav.tsx
Normal file
52
src/components/Nav/Nav.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import NavLink from './NavLink';
|
||||||
|
import config from '../../config';
|
||||||
|
import { Flex, Box } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Nav component
|
||||||
|
* Component for navigating the application.
|
||||||
|
*/
|
||||||
|
export type ActiveLinkState = {
|
||||||
|
title: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Nav: React.FC = () => {
|
||||||
|
const [activeLink, setActiveLink] = useState<ActiveLinkState>({ title: null });
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleRoute = (location: string): void => history.push(location);
|
||||||
|
const handleOnHoverLink = (title: string): void => setActiveLink({ title });
|
||||||
|
const handleOnLeaveLink = (): void => setActiveLink({ title: null });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display='block' position='fixed' left={0} right={0} top={0} width='100%' height='100%' maxWidth='18rem'>
|
||||||
|
<Box top='4rem' position='relative' overflowY='auto' borderRightWidth={1}>
|
||||||
|
<Flex
|
||||||
|
as='nav'
|
||||||
|
flexDirection='column'
|
||||||
|
alignItems='flex-start'
|
||||||
|
textDecoration='none'
|
||||||
|
height='calc(100vh - 4rem)'
|
||||||
|
padding='1.5rem 1.2rem'
|
||||||
|
>
|
||||||
|
{config.links.map((link, i) => (
|
||||||
|
<NavLink
|
||||||
|
handleOnHoverLink={handleOnHoverLink}
|
||||||
|
handleOnLeaveLink={handleOnLeaveLink}
|
||||||
|
handleRoute={handleRoute}
|
||||||
|
activeLink={activeLink}
|
||||||
|
history={history}
|
||||||
|
link={link}
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Nav;
|
||||||
65
src/components/Nav/NavLink.tsx
Normal file
65
src/components/Nav/NavLink.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import type { History } from 'history';
|
||||||
|
import type { LinkType } from '../../interfaces';
|
||||||
|
import type { ActiveLinkState } from './Nav';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* NavLink component
|
||||||
|
* Returns the link component for navigation.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface NavLinkProps {
|
||||||
|
handleOnHoverLink: (title: string) => void;
|
||||||
|
handleOnLeaveLink: () => void;
|
||||||
|
handleRoute: (location: string) => void;
|
||||||
|
history: History;
|
||||||
|
activeLink: ActiveLinkState;
|
||||||
|
link: LinkType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavLink: React.FC<NavLinkProps> = ({
|
||||||
|
handleOnHoverLink,
|
||||||
|
handleOnLeaveLink,
|
||||||
|
handleRoute,
|
||||||
|
history,
|
||||||
|
activeLink,
|
||||||
|
link,
|
||||||
|
}) => {
|
||||||
|
const onMouseEnter = () => handleOnHoverLink(link.title);
|
||||||
|
const onClick = () => handleRoute(link.location);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
as='a'
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={handleOnLeaveLink}
|
||||||
|
onClick={onClick}
|
||||||
|
display='flex'
|
||||||
|
margin='3px 0'
|
||||||
|
width='100%'
|
||||||
|
border='none'
|
||||||
|
aria-label={link.title}
|
||||||
|
role='link'
|
||||||
|
cursor='pointer'
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
color={history.location.pathname === link.location || activeLink.title === link.title ? 'blue.500' : ''}
|
||||||
|
backgroundColor={activeLink.title === link.title ? '#EBF8FF' : ''}
|
||||||
|
alignItems='center'
|
||||||
|
borderRadius='9999px'
|
||||||
|
display='flex'
|
||||||
|
padding='10px 15px'
|
||||||
|
fontWeight='500'
|
||||||
|
transition='all .1s'
|
||||||
|
>
|
||||||
|
<Box as={link.icon} boxSize='26px' mr={3} />
|
||||||
|
{link.title}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavLink;
|
||||||
@ -10,19 +10,14 @@ import { observer } from 'mobx-react';
|
|||||||
* 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> = ({
|
const PrivateRoute: React.FC<IPrivateRouteProps> = ({ children, component: Component, title, ...options }) => {
|
||||||
children,
|
|
||||||
component: Component,
|
|
||||||
title,
|
|
||||||
...options
|
|
||||||
}) => {
|
|
||||||
const { authStore } = useRootStore();
|
const { authStore } = useRootStore();
|
||||||
const isLoggedIn = authStore.isLoggedIn;
|
const isLoggedIn = authStore.isLoggedIn;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
{...options}
|
{...options}
|
||||||
render={(props) =>
|
render={props =>
|
||||||
isLoggedIn ? (
|
isLoggedIn ? (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -31,9 +26,7 @@ const PrivateRoute: React.FC<IPrivateRouteProps> = ({
|
|||||||
<Component {...props} title={title} />
|
<Component {...props} title={title} />
|
||||||
</Layout>
|
</Layout>
|
||||||
) : (
|
) : (
|
||||||
<Redirect
|
<Redirect to={{ pathname: '/auth', state: { from: props.location } }} />
|
||||||
to={{ pathname: '/auth', state: { from: props.location } }}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
13
src/config.ts
Normal file
13
src/config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { MdAccountCircle, MdCollectionsBookmark, MdHome, MdMessage } from 'react-icons/md';
|
||||||
|
import { IAppConfig } from './interfaces';
|
||||||
|
|
||||||
|
const config: IAppConfig = {
|
||||||
|
links: [
|
||||||
|
{ location: '/', title: 'Home', icon: MdHome },
|
||||||
|
{ location: '/courses', title: 'Courses', icon: MdCollectionsBookmark },
|
||||||
|
{ location: '/messages', title: 'Messages', icon: MdMessage },
|
||||||
|
{ location: '/profile', title: 'Profile', icon: MdAccountCircle },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@ -1,6 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import RootStore from '../stores/RootStore';
|
import RootStore from '../stores/RootStore';
|
||||||
|
|
||||||
export const StoreContext = React.createContext<RootStore | undefined>(
|
export const StoreContext = React.createContext<RootStore | undefined>(undefined);
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|||||||
@ -1,6 +1,17 @@
|
|||||||
|
import { IconType } from 'react-icons/lib';
|
||||||
import type { RouteProps } from 'react-router-dom';
|
import type { RouteProps } from 'react-router-dom';
|
||||||
import type { RouteComponentProps } from 'react-router-dom';
|
import type { RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
|
export type LinkType = {
|
||||||
|
location: string;
|
||||||
|
title: string;
|
||||||
|
icon: IconType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IAppConfig {
|
||||||
|
links: LinkType[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IDataResult {
|
export interface IDataResult {
|
||||||
data: any;
|
data: any;
|
||||||
message: string;
|
message: string;
|
||||||
|
|||||||
@ -27,17 +27,8 @@ const AuthPage: React.FC<IPageProps> = () => {
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Auth</title>
|
<title>Auth</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Flex
|
<Flex minHeight='100vh' align='center' justifyContent='center' padding='5'>
|
||||||
minHeight='100vh'
|
<Box p={8} width='full' maxWidth='450px' borderWidth={1} borderRadius={8}>
|
||||||
align='center'
|
|
||||||
justifyContent='center'
|
|
||||||
padding='5'>
|
|
||||||
<Box
|
|
||||||
p={8}
|
|
||||||
width='full'
|
|
||||||
maxWidth='450px'
|
|
||||||
borderWidth={1}
|
|
||||||
borderRadius={8}>
|
|
||||||
<Box textAlign='center'>
|
<Box textAlign='center'>
|
||||||
<Flex justifyContent='center'>
|
<Flex justifyContent='center'>
|
||||||
<MdSchool fill='blue' size='64px' />
|
<MdSchool fill='blue' size='64px' />
|
||||||
|
|||||||
40
src/pages/courses.tsx
Normal file
40
src/pages/courses.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React, { useEffect } 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<IPageProps> = ({ title }) => {
|
||||||
|
const { authStore } = useRootStore();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authStore.isLoggedIn) {
|
||||||
|
history.push('/auth');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
||||||
|
<BreadcrumbItem isCurrentPage>
|
||||||
|
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</Breadcrumb>
|
||||||
|
<Box mt='10'>
|
||||||
|
<Box>
|
||||||
|
<Heading as='h1'>Courses page</Heading>
|
||||||
|
<Grid templateColumns='3fr 1fr' mt='25px'>
|
||||||
|
...
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CoursesPage;
|
||||||
@ -1,12 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import {
|
import { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from '@chakra-ui/react';
|
||||||
Box,
|
|
||||||
Heading,
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
Grid,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { IPageProps } from '../interfaces';
|
import { IPageProps } from '../interfaces';
|
||||||
import { useRootStore } from '../hooks/useRootStore';
|
import { useRootStore } from '../hooks/useRootStore';
|
||||||
@ -32,7 +25,7 @@ const MainPage: React.FC<IPageProps> = ({ title }) => {
|
|||||||
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
|
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<Box marginTop={10}>
|
<Box mt='10'>
|
||||||
<Box>
|
<Box>
|
||||||
<Heading as='h1'>Main page</Heading>
|
<Heading as='h1'>Main page</Heading>
|
||||||
<Grid templateColumns='3fr 1fr' mt='25px'>
|
<Grid templateColumns='3fr 1fr' mt='25px'>
|
||||||
|
|||||||
40
src/pages/messages.tsx
Normal file
40
src/pages/messages.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React, { useEffect } 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<IPageProps> = ({ title }) => {
|
||||||
|
const { authStore } = useRootStore();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authStore.isLoggedIn) {
|
||||||
|
history.push('/auth');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
||||||
|
<BreadcrumbItem isCurrentPage>
|
||||||
|
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</Breadcrumb>
|
||||||
|
<Box mt='10'>
|
||||||
|
<Box>
|
||||||
|
<Heading as='h1'>Messages page</Heading>
|
||||||
|
<Grid templateColumns='3fr 1fr' mt='25px'>
|
||||||
|
...
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessagesPage;
|
||||||
40
src/pages/profile.tsx
Normal file
40
src/pages/profile.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React, { useEffect } 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile page
|
||||||
|
*/
|
||||||
|
const ProfilePage: React.FC<IPageProps> = ({ title }) => {
|
||||||
|
const { authStore } = useRootStore();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authStore.isLoggedIn) {
|
||||||
|
history.push('/auth');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
||||||
|
<BreadcrumbItem isCurrentPage>
|
||||||
|
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</Breadcrumb>
|
||||||
|
<Box mt='10'>
|
||||||
|
<Box>
|
||||||
|
<Heading as='h1'>Profile page</Heading>
|
||||||
|
<Grid templateColumns='3fr 1fr' mt='25px'>
|
||||||
|
...
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilePage;
|
||||||
@ -8,7 +8,5 @@ import RootStore from '../stores/RootStore';
|
|||||||
export const RootStoreProvider = ({ children }: { children: ReactNode }) => {
|
export const RootStoreProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const store = new RootStore();
|
const store = new RootStore();
|
||||||
|
|
||||||
return (
|
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
|
||||||
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { ReportHandler } from "web-vitals";
|
import { ReportHandler } from 'web-vitals';
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
getCLS(onPerfEntry);
|
getCLS(onPerfEntry);
|
||||||
getFID(onPerfEntry);
|
getFID(onPerfEntry);
|
||||||
getFCP(onPerfEntry);
|
getFCP(onPerfEntry);
|
||||||
|
|||||||
@ -19,29 +19,17 @@ export default class ApiService {
|
|||||||
/**
|
/**
|
||||||
* Response
|
* Response
|
||||||
*/
|
*/
|
||||||
this.api.interceptors.response.use(undefined, (error) => {
|
this.api.interceptors.response.use(undefined, error => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
/**
|
|
||||||
* Internal Server Error 500
|
|
||||||
*/
|
|
||||||
if (error.response.status === 500) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unauthorized Error 401
|
* Unauthorized Error 401
|
||||||
*/
|
*/
|
||||||
if (error.response.status === 401) {
|
if (error.response.status === 401) {
|
||||||
if (this.root.authStore.isLoggedIn) {
|
if (this.root.authStore.isLoggedIn) {
|
||||||
|
this.root.userStore.user = null;
|
||||||
this.root.authStore.setIsLoggedIn(false);
|
this.root.authStore.setIsLoggedIn(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clear error state from root */
|
|
||||||
if (this.root.networkApiError !== undefined) {
|
|
||||||
this.root.setNetworkApiError(undefined);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.root.setNetworkApiError(error);
|
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -23,10 +23,7 @@ export default class AuthService {
|
|||||||
* @param password User password
|
* @param password User password
|
||||||
* @returns Login result
|
* @returns Login result
|
||||||
*/
|
*/
|
||||||
public async requestLogin(
|
public async requestLogin(login: string, password: string): Promise<ILoginResult> {
|
||||||
login: string,
|
|
||||||
password: string
|
|
||||||
): Promise<ILoginResult> {
|
|
||||||
const result = await this.api.post('v1/auth/login', {
|
const result = await this.api.post('v1/auth/login', {
|
||||||
login,
|
login,
|
||||||
password,
|
password,
|
||||||
|
|||||||
@ -2,4 +2,4 @@
|
|||||||
// allows you to do things like:
|
// allows you to do things like:
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
import "@testing-library/jest-dom";
|
import '@testing-library/jest-dom';
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import UserStore from './UserStore';
|
|||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
|
||||||
export default class RootStore {
|
export default class RootStore {
|
||||||
public networkApiError: any;
|
|
||||||
|
|
||||||
public authStore: AuthStore;
|
public authStore: AuthStore;
|
||||||
|
|
||||||
public userStore: UserStore;
|
public userStore: UserStore;
|
||||||
@ -35,8 +33,4 @@ export default class RootStore {
|
|||||||
this.authService = new AuthService(this.apiService.api);
|
this.authService = new AuthService(this.apiService.api);
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setNetworkApiError(error: any) {
|
|
||||||
this.networkApiError = error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from 'react';
|
||||||
import { render, RenderOptions } from "@testing-library/react";
|
import { render, RenderOptions } from '@testing-library/react';
|
||||||
import { ChakraProvider, theme } from "@chakra-ui/react";
|
import { ChakraProvider, theme } from '@chakra-ui/react';
|
||||||
|
|
||||||
const AllProviders = ({ children }: { children?: React.ReactNode }) => (
|
const AllProviders = ({ children }: { children?: React.ReactNode }) => (
|
||||||
<ChakraProvider theme={theme}>{children}</ChakraProvider>
|
<ChakraProvider theme={theme}>{children}</ChakraProvider>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user