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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "6.0.9",
|
||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"mobx-react": "^7.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-boundary": "^3.1.3",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.12.2",
|
||||
"react-icons": "^3.11.0",
|
||||
|
||||
30
src/App.tsx
30
src/App.tsx
@ -1,42 +1,34 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
|
||||
import PrivateRoute from './components/PrivateRoute/PrivateRoute';
|
||||
import { useRootStore } from './hooks/useRootStore';
|
||||
import { observer } from 'mobx-react';
|
||||
import ErrorFallback from './components/ErrorFallback';
|
||||
|
||||
/**
|
||||
* Pages
|
||||
*/
|
||||
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 NotFoundPage from './pages/404';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { autorun } from 'mobx';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
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 (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<Router>
|
||||
<Switch>
|
||||
<PrivateRoute exact path='/' component={MainPage} title='Main' />
|
||||
<PrivateRoute path='/courses' component={CoursesPage} title='Courses' />
|
||||
<PrivateRoute path='/messages' component={MessagesPage} title='Messages' />
|
||||
<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 { MdAccountCircle, MdVpnKey } from 'react-icons/md';
|
||||
import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
InputGroup,
|
||||
Input,
|
||||
InputLeftElement,
|
||||
Button,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { FormControl, FormLabel, InputGroup, Input, InputLeftElement, Button, Icon, useToast } from '@chakra-ui/react';
|
||||
import { useRootStore } from '../../hooks/useRootStore';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
|
||||
type AuthInputs = {
|
||||
login: string;
|
||||
@ -23,13 +16,35 @@ const AuthForm: React.FC = () => {
|
||||
const { register, reset, handleSubmit } = useForm<AuthInputs>();
|
||||
const { authStore } = useRootStore();
|
||||
const history = useHistory();
|
||||
const handleError = useErrorHandler();
|
||||
const toast = useToast();
|
||||
|
||||
const onSubmit: SubmitHandler<AuthInputs> = async (data) => {
|
||||
const onSubmit: SubmitHandler<AuthInputs> = async data => {
|
||||
try {
|
||||
await authStore.login(data.login, data.password);
|
||||
history.push('/');
|
||||
toast({
|
||||
title: `👋 Welcome back!`,
|
||||
description: 'You are successfully logged in.',
|
||||
isClosable: true,
|
||||
status: 'success',
|
||||
});
|
||||
} 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 {
|
||||
reset();
|
||||
}
|
||||
@ -41,22 +56,14 @@ const AuthForm: React.FC = () => {
|
||||
<FormLabel>Login</FormLabel>
|
||||
<InputGroup>
|
||||
<InputLeftElement children={<Icon as={MdAccountCircle} />} />
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='email@mail.com'
|
||||
{...register('login', { required: true })}
|
||||
/>
|
||||
<Input type='text' placeholder='Type your login' {...register('login', { required: true })} />
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
<FormControl mt={5} isRequired={true}>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<InputGroup>
|
||||
<InputLeftElement children={<Icon as={MdVpnKey} />} />
|
||||
<Input
|
||||
type='password'
|
||||
placeholder='********'
|
||||
{...register('password', { required: true })}
|
||||
/>
|
||||
<Input type='password' placeholder='********' {...register('password', { required: true })} />
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
<Button
|
||||
@ -66,7 +73,8 @@ const AuthForm: React.FC = () => {
|
||||
colorScheme='blue'
|
||||
variant='outline'
|
||||
width='full'
|
||||
mt={4}>
|
||||
mt={4}
|
||||
>
|
||||
Log in
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@ -1,26 +1,21 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
useColorMode,
|
||||
useColorModeValue,
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
} from "@chakra-ui/react";
|
||||
import { FaMoon, FaSun } from "react-icons/fa";
|
||||
import * as React from 'react';
|
||||
import { useColorMode, 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 text = useColorModeValue("dark", "light");
|
||||
const text = useColorModeValue('dark', 'light');
|
||||
const SwitchIcon = useColorModeValue(FaMoon, FaSun);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size="md"
|
||||
fontSize="lg"
|
||||
variant="ghost"
|
||||
color="current"
|
||||
marginLeft="2"
|
||||
size='md'
|
||||
fontSize='lg'
|
||||
variant='ghost'
|
||||
color='current'
|
||||
marginLeft='2'
|
||||
onClick={toggleColorMode}
|
||||
icon={<SwitchIcon />}
|
||||
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}
|
||||
width='100%'
|
||||
height='4rem'
|
||||
zIndex={4}>
|
||||
zIndex={4}
|
||||
>
|
||||
<Flex
|
||||
justifyContent='space-between'
|
||||
width='100%'
|
||||
height='100%'
|
||||
alignItems='center'
|
||||
paddingLeft='1.5rem'
|
||||
paddingRight='1.5rem'>
|
||||
paddingRight='1.5rem'
|
||||
>
|
||||
<AppName />
|
||||
<Flex>
|
||||
<UserNotifications />
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
Avatar,
|
||||
Text,
|
||||
Link,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { MdSettings, MdExitToApp } from 'react-icons/md';
|
||||
import { observer } from 'mobx-react';
|
||||
@ -21,13 +22,13 @@ import { useHistory } from 'react-router-dom';
|
||||
const UserInfo: React.FC = () => {
|
||||
const { authStore } = useRootStore();
|
||||
const history = useHistory();
|
||||
const toast = useToast();
|
||||
|
||||
const onLogoutHandler = async (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
const onLogoutHandler = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
try {
|
||||
await authStore.logout();
|
||||
history.push('/auth');
|
||||
toast({ title: 'You are logged out.', isClosable: true, status: 'info' });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@ -22,7 +22,8 @@ const UserNotifications: React.FC = () => {
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
borderRadius: '100%',
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Box as={MdNotifications} boxSize='21px' />
|
||||
</Button>
|
||||
);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Flex, Box } from '@chakra-ui/react';
|
||||
import Header from '../Header/Header';
|
||||
import Nav from '../Nav/Nav';
|
||||
|
||||
/**
|
||||
*
|
||||
@ -11,17 +12,10 @@ const Layout: React.FC = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Nav />
|
||||
<Flex minH='100vh' flexDirection='column'>
|
||||
<Box pl='18rem' mt='4rem' mb='4rem' flex='1 0 auto'>
|
||||
<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}
|
||||
</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,
|
||||
* then a redirect to "/auth" route.
|
||||
*/
|
||||
const PrivateRoute: React.FC<IPrivateRouteProps> = ({
|
||||
children,
|
||||
component: Component,
|
||||
title,
|
||||
...options
|
||||
}) => {
|
||||
const PrivateRoute: React.FC<IPrivateRouteProps> = ({ children, component: Component, title, ...options }) => {
|
||||
const { authStore } = useRootStore();
|
||||
const isLoggedIn = authStore.isLoggedIn;
|
||||
|
||||
return (
|
||||
<Route
|
||||
{...options}
|
||||
render={(props) =>
|
||||
render={props =>
|
||||
isLoggedIn ? (
|
||||
<Layout>
|
||||
<Helmet>
|
||||
@ -31,9 +26,7 @@ const PrivateRoute: React.FC<IPrivateRouteProps> = ({
|
||||
<Component {...props} title={title} />
|
||||
</Layout>
|
||||
) : (
|
||||
<Redirect
|
||||
to={{ pathname: '/auth', state: { from: props.location } }}
|
||||
/>
|
||||
<Redirect 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 RootStore from '../stores/RootStore';
|
||||
|
||||
export const StoreContext = React.createContext<RootStore | undefined>(
|
||||
undefined
|
||||
);
|
||||
export const StoreContext = React.createContext<RootStore | undefined>(undefined);
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
import { IconType } from 'react-icons/lib';
|
||||
import type { RouteProps } 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 {
|
||||
data: any;
|
||||
message: string;
|
||||
|
||||
@ -27,17 +27,8 @@ const AuthPage: React.FC<IPageProps> = () => {
|
||||
<Helmet>
|
||||
<title>Auth</title>
|
||||
</Helmet>
|
||||
<Flex
|
||||
minHeight='100vh'
|
||||
align='center'
|
||||
justifyContent='center'
|
||||
padding='5'>
|
||||
<Box
|
||||
p={8}
|
||||
width='full'
|
||||
maxWidth='450px'
|
||||
borderWidth={1}
|
||||
borderRadius={8}>
|
||||
<Flex minHeight='100vh' align='center' justifyContent='center' padding='5'>
|
||||
<Box p={8} width='full' maxWidth='450px' borderWidth={1} borderRadius={8}>
|
||||
<Box textAlign='center'>
|
||||
<Flex justifyContent='center'>
|
||||
<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 {
|
||||
Box,
|
||||
Heading,
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
Grid,
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from '@chakra-ui/react';
|
||||
|
||||
import { IPageProps } from '../interfaces';
|
||||
import { useRootStore } from '../hooks/useRootStore';
|
||||
@ -32,7 +25,7 @@ const MainPage: React.FC<IPageProps> = ({ title }) => {
|
||||
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
<Box marginTop={10}>
|
||||
<Box mt='10'>
|
||||
<Box>
|
||||
<Heading as='h1'>Main page</Heading>
|
||||
<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 }) => {
|
||||
const store = new RootStore();
|
||||
|
||||
return (
|
||||
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
|
||||
);
|
||||
return <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) => {
|
||||
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);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
|
||||
@ -19,29 +19,17 @@ export default class ApiService {
|
||||
/**
|
||||
* Response
|
||||
*/
|
||||
this.api.interceptors.response.use(undefined, (error) => {
|
||||
this.api.interceptors.response.use(undefined, error => {
|
||||
if (error.response) {
|
||||
/**
|
||||
* Internal Server Error 500
|
||||
*/
|
||||
if (error.response.status === 500) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unauthorized Error 401
|
||||
*/
|
||||
if (error.response.status === 401) {
|
||||
if (this.root.authStore.isLoggedIn) {
|
||||
this.root.userStore.user = null;
|
||||
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);
|
||||
});
|
||||
|
||||
@ -23,10 +23,7 @@ export default class AuthService {
|
||||
* @param password User password
|
||||
* @returns Login result
|
||||
*/
|
||||
public async requestLogin(
|
||||
login: string,
|
||||
password: string
|
||||
): Promise<ILoginResult> {
|
||||
public async requestLogin(login: string, password: string): Promise<ILoginResult> {
|
||||
const result = await this.api.post('v1/auth/login', {
|
||||
login,
|
||||
password,
|
||||
|
||||
@ -2,4 +2,4 @@
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// 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';
|
||||
|
||||
export default class RootStore {
|
||||
public networkApiError: any;
|
||||
|
||||
public authStore: AuthStore;
|
||||
|
||||
public userStore: UserStore;
|
||||
@ -35,8 +33,4 @@ export default class RootStore {
|
||||
this.authService = new AuthService(this.apiService.api);
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
public setNetworkApiError(error: any) {
|
||||
this.networkApiError = error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { render, RenderOptions } from "@testing-library/react";
|
||||
import { ChakraProvider, theme } from "@chakra-ui/react";
|
||||
import * as React from 'react';
|
||||
import { render, RenderOptions } from '@testing-library/react';
|
||||
import { ChakraProvider, theme } from '@chakra-ui/react';
|
||||
|
||||
const AllProviders = ({ children }: { children?: React.ReactNode }) => (
|
||||
<ChakraProvider theme={theme}>{children}</ChakraProvider>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user