diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..d7e0a43
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,10 @@
+{
+ "singleQuote": true,
+ "useTabs": false,
+ "tabWidth": 2,
+ "semi": true,
+ "jsxSingleQuote": true,
+ "arrowParens": "avoid",
+ "endOfLine": "auto",
+ "printWidth": 120
+}
diff --git a/package-lock.json b/package-lock.json
index 96ff73e..b8e1f4e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index d1d27b3..b3bd5d6 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/App.tsx b/src/App.tsx
index e6c102f..fadae32 100644
--- a/src/App.tsx
+++ b/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 (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/components/Auth/AuthForm.tsx b/src/components/Auth/AuthForm.tsx
index df965ce..75ce643 100644
--- a/src/components/Auth/AuthForm.tsx
+++ b/src/components/Auth/AuthForm.tsx
@@ -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();
const { authStore } = useRootStore();
const history = useHistory();
+ const handleError = useErrorHandler();
+ const toast = useToast();
- const onSubmit: SubmitHandler = async (data) => {
+ const onSubmit: SubmitHandler = 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 = () => {
Login
} />
-
+
Password
} />
-
+
diff --git a/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx b/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx
index 2862482..4560b32 100644
--- a/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx
+++ b/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx
@@ -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;
+type ColorModeSwitcherProps = Omit;
-export const ColorModeSwitcher: React.FC = (props) => {
+export const ColorModeSwitcher: React.FC = props => {
const { toggleColorMode } = useColorMode();
- const text = useColorModeValue("dark", "light");
+ const text = useColorModeValue('dark', 'light');
const SwitchIcon = useColorModeValue(FaMoon, FaSun);
return (
}
aria-label={`Switch to ${text} mode`}
diff --git a/src/components/ErrorFallback.tsx b/src/components/ErrorFallback.tsx
new file mode 100644
index 0000000..e0a66b2
--- /dev/null
+++ b/src/components/ErrorFallback.tsx
@@ -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 (
+
+
+
+
+
+ Something went wrong
+
+ Sorry, something went wrong. Reload page and try your request again later.
+
+ );
+};
+
+export default ErrorFallback;
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index bb6e3fe..bf2cddd 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -16,14 +16,16 @@ const Header: React.FC = () => {
borderBottomWidth={1}
width='100%'
height='4rem'
- zIndex={4}>
+ zIndex={4}
+ >
+ paddingRight='1.5rem'
+ >
diff --git a/src/components/Header/User/UserInfo.tsx b/src/components/Header/User/UserInfo.tsx
index c99f886..fd14f1c 100644
--- a/src/components/Header/User/UserInfo.tsx
+++ b/src/components/Header/User/UserInfo.tsx
@@ -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
- ) => {
+ 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);
}
diff --git a/src/components/Header/User/UserNotifications.tsx b/src/components/Header/User/UserNotifications.tsx
index a547239..65aa9a8 100644
--- a/src/components/Header/User/UserNotifications.tsx
+++ b/src/components/Header/User/UserNotifications.tsx
@@ -22,7 +22,8 @@ const UserNotifications: React.FC = () => {
width: '15px',
height: '15px',
borderRadius: '100%',
- }}>
+ }}
+ >
);
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
index d4040d0..f19262d 100644
--- a/src/components/Layout/Layout.tsx
+++ b/src/components/Layout/Layout.tsx
@@ -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 (
<>
+
-
+
{children}
diff --git a/src/components/Nav/Nav.tsx b/src/components/Nav/Nav.tsx
new file mode 100644
index 0000000..9582e75
--- /dev/null
+++ b/src/components/Nav/Nav.tsx
@@ -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({ 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 (
+
+
+
+ {config.links.map((link, i) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default Nav;
diff --git a/src/components/Nav/NavLink.tsx b/src/components/Nav/NavLink.tsx
new file mode 100644
index 0000000..876ed9d
--- /dev/null
+++ b/src/components/Nav/NavLink.tsx
@@ -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 = ({
+ handleOnHoverLink,
+ handleOnLeaveLink,
+ handleRoute,
+ history,
+ activeLink,
+ link,
+}) => {
+ const onMouseEnter = () => handleOnHoverLink(link.title);
+ const onClick = () => handleRoute(link.location);
+
+ return (
+
+
+
+ {link.title}
+
+
+ );
+};
+
+export default NavLink;
diff --git a/src/components/PrivateRoute/PrivateRoute.tsx b/src/components/PrivateRoute/PrivateRoute.tsx
index d590aa0..69488bd 100644
--- a/src/components/PrivateRoute/PrivateRoute.tsx
+++ b/src/components/PrivateRoute/PrivateRoute.tsx
@@ -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 = ({
- children,
- component: Component,
- title,
- ...options
-}) => {
+const PrivateRoute: React.FC = ({ children, component: Component, title, ...options }) => {
const { authStore } = useRootStore();
const isLoggedIn = authStore.isLoggedIn;
return (
+ render={props =>
isLoggedIn ? (
@@ -31,9 +26,7 @@ const PrivateRoute: React.FC = ({
) : (
-
+
)
}
/>
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..fceb7fb
--- /dev/null
+++ b/src/config.ts
@@ -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;
diff --git a/src/contexts/index.ts b/src/contexts/index.ts
index 403067e..633275f 100644
--- a/src/contexts/index.ts
+++ b/src/contexts/index.ts
@@ -1,6 +1,4 @@
import React from 'react';
import RootStore from '../stores/RootStore';
-export const StoreContext = React.createContext(
- undefined
-);
+export const StoreContext = React.createContext(undefined);
diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts
index 651050e..4d2eee5 100644
--- a/src/interfaces/index.ts
+++ b/src/interfaces/index.ts
@@ -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;
diff --git a/src/pages/auth.tsx b/src/pages/auth.tsx
index 26f6a25..8c92fbf 100644
--- a/src/pages/auth.tsx
+++ b/src/pages/auth.tsx
@@ -27,17 +27,8 @@ const AuthPage: React.FC = () => {
Auth
-
-
+
+
diff --git a/src/pages/courses.tsx b/src/pages/courses.tsx
new file mode 100644
index 0000000..faa4663
--- /dev/null
+++ b/src/pages/courses.tsx
@@ -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 = ({ title }) => {
+ const { authStore } = useRootStore();
+ const history = useHistory();
+
+ useEffect(() => {
+ if (!authStore.isLoggedIn) {
+ history.push('/auth');
+ }
+ });
+
+ return (
+ <>
+
+
+ {title}
+
+
+
+
+ Courses page
+
+ ...
+
+
+
+ >
+ );
+};
+
+export default CoursesPage;
diff --git a/src/pages/main.tsx b/src/pages/main.tsx
index 4a0bbce..d3aa93b 100644
--- a/src/pages/main.tsx
+++ b/src/pages/main.tsx
@@ -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 = ({ title }) => {
{title}
-
+
Main page
diff --git a/src/pages/messages.tsx b/src/pages/messages.tsx
new file mode 100644
index 0000000..21b39df
--- /dev/null
+++ b/src/pages/messages.tsx
@@ -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 = ({ title }) => {
+ const { authStore } = useRootStore();
+ const history = useHistory();
+
+ useEffect(() => {
+ if (!authStore.isLoggedIn) {
+ history.push('/auth');
+ }
+ });
+
+ return (
+ <>
+
+
+ {title}
+
+
+
+
+ Messages page
+
+ ...
+
+
+
+ >
+ );
+};
+
+export default MessagesPage;
diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx
new file mode 100644
index 0000000..30a6cb3
--- /dev/null
+++ b/src/pages/profile.tsx
@@ -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 = ({ title }) => {
+ const { authStore } = useRootStore();
+ const history = useHistory();
+
+ useEffect(() => {
+ if (!authStore.isLoggedIn) {
+ history.push('/auth');
+ }
+ });
+
+ return (
+ <>
+
+
+ {title}
+
+
+
+
+ Profile page
+
+ ...
+
+
+
+ >
+ );
+};
+
+export default ProfilePage;
diff --git a/src/providers/index.tsx b/src/providers/index.tsx
index fa5ef56..5adb72a 100644
--- a/src/providers/index.tsx
+++ b/src/providers/index.tsx
@@ -8,7 +8,5 @@ import RootStore from '../stores/RootStore';
export const RootStoreProvider = ({ children }: { children: ReactNode }) => {
const store = new RootStore();
- return (
- {children}
- );
+ return {children};
};
diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts
index 5fa3583..49a2a16 100644
--- a/src/reportWebVitals.ts
+++ b/src/reportWebVitals.ts
@@ -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);
diff --git a/src/services/ApiService.ts b/src/services/ApiService.ts
index 001de3f..0f591a0 100644
--- a/src/services/ApiService.ts
+++ b/src/services/ApiService.ts
@@ -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);
});
diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts
index 6fb6aa5..a653ba4 100644
--- a/src/services/AuthService.ts
+++ b/src/services/AuthService.ts
@@ -23,10 +23,7 @@ export default class AuthService {
* @param password User password
* @returns Login result
*/
- public async requestLogin(
- login: string,
- password: string
- ): Promise {
+ public async requestLogin(login: string, password: string): Promise {
const result = await this.api.post('v1/auth/login', {
login,
password,
diff --git a/src/setupTests.ts b/src/setupTests.ts
index 1dd407a..8f2609b 100644
--- a/src/setupTests.ts
+++ b/src/setupTests.ts
@@ -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';
diff --git a/src/stores/RootStore.ts b/src/stores/RootStore.ts
index d0b0d73..61c60ab 100644
--- a/src/stores/RootStore.ts
+++ b/src/stores/RootStore.ts
@@ -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;
- }
}
diff --git a/src/test-utils.tsx b/src/test-utils.tsx
index f7c76be..ee71f70 100644
--- a/src/test-utils.tsx
+++ b/src/test-utils.tsx
@@ -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 }) => (
{children}