diff --git a/src/App.tsx b/src/App.tsx
index f3bee20..e6c102f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,21 +1,43 @@
-import * as React from 'react';
+import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import PrivateRoute from './components/PrivateRoute/PrivateRoute';
-import { ChakraProvider, theme } from '@chakra-ui/react';
+import { useRootStore } from './hooks/useRootStore';
+import { observer } from 'mobx-react';
/**
* Pages
*/
import MainPage from './pages/main';
import AuthPage from './pages/auth';
+import NotFoundPage from './pages/404';
+import { useToast } from '@chakra-ui/react';
+import { autorun } from 'mobx';
-export 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 (
+
-
-);
+ );
+};
+
+export default observer(App);
diff --git a/src/components/Auth/AuthForm.tsx b/src/components/Auth/AuthForm.tsx
index 629928f..df965ce 100644
--- a/src/components/Auth/AuthForm.tsx
+++ b/src/components/Auth/AuthForm.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React from 'react';
import { MdAccountCircle, MdVpnKey } from 'react-icons/md';
import {
FormControl,
@@ -12,44 +12,39 @@ import {
import { useRootStore } from '../../hooks/useRootStore';
import { useHistory } from 'react-router-dom';
import { observer } from 'mobx-react';
+import { SubmitHandler, useForm } from 'react-hook-form';
-type AuthFormState = {
+type AuthInputs = {
login: string;
password: string;
};
const AuthForm: React.FC = () => {
- const [authFormState, setAuthFormState] = useState({
- login: '',
- password: '',
- });
+ const { register, reset, handleSubmit } = useForm();
const { authStore } = useRootStore();
const history = useHistory();
- const handleLoginForm = async (e: React.FormEvent) => {
- e.preventDefault();
- const { login, password } = authFormState;
- await authStore.login(login, password);
- history.push('/');
+ const onSubmit: SubmitHandler = async (data) => {
+ try {
+ await authStore.login(data.login, data.password);
+ history.push('/');
+ } catch (error) {
+ console.log('error while log in ');
+ } finally {
+ reset();
+ }
};
return (
-
- )
-}
+ );
+};
-export default observer(AuthForm)
\ No newline at end of file
+export default observer(AuthForm);
diff --git a/src/components/Header/AppName.tsx b/src/components/Header/AppName.tsx
new file mode 100644
index 0000000..1389dbd
--- /dev/null
+++ b/src/components/Header/AppName.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Flex, Box, Text, Heading } from '@chakra-ui/react';
+import { MdSchool } from 'react-icons/md';
+
+const AppName: React.FC = () => {
+ return (
+
+
+
+
+
+
+ App name
+
+ App description
+
+
+ );
+};
+
+export default AppName;
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 0000000..bb6e3fe
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { Flex, Box } from '@chakra-ui/react';
+import AppName from './AppName';
+import UserInfo from './User/UserInfo';
+import UserNotifications from './User/UserNotifications';
+
+const Header: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Header;
diff --git a/src/components/Header/User/UserInfo.tsx b/src/components/Header/User/UserInfo.tsx
new file mode 100644
index 0000000..c99f886
--- /dev/null
+++ b/src/components/Header/User/UserInfo.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import {
+ Menu,
+ MenuGroup,
+ MenuList,
+ MenuDivider,
+ MenuButton,
+ MenuItem,
+ Button,
+ Flex,
+ Box,
+ Avatar,
+ Text,
+ Link,
+} from '@chakra-ui/react';
+import { MdSettings, MdExitToApp } from 'react-icons/md';
+import { observer } from 'mobx-react';
+import { useRootStore } from '../../../hooks/useRootStore';
+import { useHistory } from 'react-router-dom';
+
+const UserInfo: React.FC = () => {
+ const { authStore } = useRootStore();
+ const history = useHistory();
+
+ const onLogoutHandler = async (
+ e: React.MouseEvent
+ ) => {
+ try {
+ await authStore.logout();
+ history.push('/auth');
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default observer(UserInfo);
diff --git a/src/components/Header/User/UserNotifications.tsx b/src/components/Header/User/UserNotifications.tsx
new file mode 100644
index 0000000..a547239
--- /dev/null
+++ b/src/components/Header/User/UserNotifications.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { Box } from '@chakra-ui/layout';
+import { Button } from '@chakra-ui/button';
+import { MdNotifications } from 'react-icons/md';
+
+const UserNotifications: React.FC = () => {
+ return (
+
+ );
+};
+
+export default UserNotifications;
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
index 320dd80..d4040d0 100644
--- a/src/components/Layout/Layout.tsx
+++ b/src/components/Layout/Layout.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { Flex, Box } from '@chakra-ui/react';
+import Header from '../Header/Header';
/**
*
@@ -8,21 +9,24 @@ import { Flex, Box } from '@chakra-ui/react';
*/
const Layout: React.FC = ({ children }) => {
return (
-
-
-
- {children}
+ <>
+
+
+
+
+ {children}
+
-
-
+
+ >
);
};
diff --git a/src/index.tsx b/src/index.tsx
index c795ec6..41d959e 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,7 +1,7 @@
-import { ColorModeScript } from '@chakra-ui/react';
+import { ChakraProvider, ColorModeScript, theme } from '@chakra-ui/react';
import * as React from 'react';
import ReactDOM from 'react-dom';
-import { App } from './App';
+import App from './App';
import { RootStoreProvider } from './providers';
import reportWebVitals from './reportWebVitals';
@@ -9,7 +9,9 @@ ReactDOM.render(
<>
-
+
+
+
>,
document.getElementById('root')
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
new file mode 100644
index 0000000..a7f359f
--- /dev/null
+++ b/src/pages/404.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import Helmet from 'react-helmet';
+import { Link } from 'react-router-dom';
+import { Box, Heading, Button } from '@chakra-ui/react';
+
+const NotFoundPage: React.FC = () => {
+ return (
+ <>
+
+ Error 404
+
+
+
+ Error 404
+
+
+
+
+
+ >
+ );
+};
+
+export default NotFoundPage;
diff --git a/src/pages/auth.tsx b/src/pages/auth.tsx
index 9871bd1..26f6a25 100644
--- a/src/pages/auth.tsx
+++ b/src/pages/auth.tsx
@@ -1,11 +1,8 @@
import { observer } from 'mobx-react';
-import React from 'react';
-import {
- Flex,
- Box,
-} from '@chakra-ui/react';
-import { MdSchool } from "react-icons/md";
import Helmet from 'react-helmet';
+import React, { useEffect } from 'react';
+import { Flex, Box } from '@chakra-ui/react';
+import { MdSchool } from 'react-icons/md';
import { useHistory } from 'react-router-dom';
import { useRootStore } from '../hooks/useRootStore';
import { IPageProps } from '../interfaces';
@@ -19,19 +16,24 @@ const AuthPage: React.FC = () => {
const { authStore } = useRootStore();
const history = useHistory();
- if (authStore.isLoggedIn) {
- history.push('/');
- }
+ useEffect(() => {
+ if (authStore.isLoggedIn) {
+ history.push('/');
+ }
+ });
return (
<>
Auth
-
+
= () => {
-
+
diff --git a/src/pages/main.tsx b/src/pages/main.tsx
index 272b8ae..4a0bbce 100644
--- a/src/pages/main.tsx
+++ b/src/pages/main.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import {
Box,
Heading,
@@ -9,11 +9,22 @@ import {
} from '@chakra-ui/react';
import { IPageProps } from '../interfaces';
+import { useRootStore } from '../hooks/useRootStore';
+import { useHistory } from 'react-router-dom';
/**
* Main page
*/
const MainPage: React.FC = ({ title }) => {
+ const { authStore } = useRootStore();
+ const history = useHistory();
+
+ useEffect(() => {
+ if (!authStore.isLoggedIn) {
+ history.push('/auth');
+ }
+ });
+
return (
<>
diff --git a/src/providers/index.tsx b/src/providers/index.tsx
index 1bbf1ed..fa5ef56 100644
--- a/src/providers/index.tsx
+++ b/src/providers/index.tsx
@@ -2,12 +2,13 @@ import React, { ReactNode } from 'react';
import { StoreContext } from '../contexts';
import RootStore from '../stores/RootStore';
-let store: RootStore;
-
/**
* Root store provider
*/
export const RootStoreProvider = ({ children }: { children: ReactNode }) => {
- const root = store ?? new RootStore();
- return {children};
+ const store = new RootStore();
+
+ return (
+ {children}
+ );
};
diff --git a/src/services/ApiService.ts b/src/services/ApiService.ts
index aee8973..001de3f 100644
--- a/src/services/ApiService.ts
+++ b/src/services/ApiService.ts
@@ -20,14 +20,28 @@ export default class ApiService {
* Response
*/
this.api.interceptors.response.use(undefined, (error) => {
- /**
- * Unauthorized
- */
- if (
- error.response.status === 401 ||
- error.response.data.message === '401 Unauthorized'
- ) {
- this.root.authStore.setIsLoggedIn(false);
+ 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.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 add932c..6fb6aa5 100644
--- a/src/services/AuthService.ts
+++ b/src/services/AuthService.ts
@@ -1,7 +1,7 @@
-import ApiService from './ApiService';
import { IDataResult } from '../interfaces';
+import { AxiosInstance } from 'axios';
-interface ILoginResult extends IDataResult {
+export interface ILoginResult extends IDataResult {
data: {
token: string;
type: string;
@@ -10,21 +10,39 @@ interface ILoginResult extends IDataResult {
}
export default class AuthService {
- public apiService: ApiService;
+ public api: AxiosInstance;
- constructor(apiService: ApiService) {
- this.apiService = apiService;
+ constructor(api: AxiosInstance) {
+ this.api = api;
}
+ /**
+ * Create a new token
+ *
+ * @param login User login
+ * @param password User password
+ * @returns Login result
+ */
public async requestLogin(
login: string,
password: string
): Promise {
- const result = await this.apiService.api.post('v1/auth/login', {
+ const result = await this.api.post('v1/auth/login', {
login,
password,
});
return result.data;
}
+
+ /**
+ * Revoke token
+ *
+ * @returns Logout result
+ */
+ public async requestLogout(): Promise {
+ const result = await this.api.post('v1/auth/logout');
+
+ return result.data;
+ }
}
diff --git a/src/services/UserService.ts b/src/services/UserService.ts
new file mode 100644
index 0000000..e49fef7
--- /dev/null
+++ b/src/services/UserService.ts
@@ -0,0 +1,15 @@
+import { AxiosInstance } from 'axios';
+
+export default class UserService {
+ public api: AxiosInstance;
+
+ constructor(api: AxiosInstance) {
+ this.api = api;
+ }
+
+ public async fetchMe(): Promise {
+ const result = await this.api.get('v1/me');
+
+ return result.data;
+ }
+}
diff --git a/src/stores/AuthStore.ts b/src/stores/AuthStore.ts
index 0ffb94b..a111521 100644
--- a/src/stores/AuthStore.ts
+++ b/src/stores/AuthStore.ts
@@ -9,6 +9,8 @@ export default class AuthStore {
public isLoggedIn: boolean = !!Cookies.get('logged_in');
+ public loading: boolean = false;
+
constructor(root: RootStore) {
this.root = root;
makeAutoObservable(this);
@@ -19,17 +21,44 @@ export default class AuthStore {
this.isLoggedIn = value;
}
+ public setLoading(value: boolean) {
+ this.loading = value;
+ }
+
public setToken(token: string) {
Cookies.set('logged_in', 'true');
this.token = token;
}
public async login(login: string, password: string) {
- const result = await this.root.authService.requestLogin(login, password);
+ try {
+ this.setLoading(true);
+ const result = await this.root.authService.requestLogin(login, password);
- this.setIsLoggedIn(true);
- this.setToken(result.data.token);
+ this.setIsLoggedIn(true);
+ this.setToken(result.data.token);
+
+ return result;
+ } catch (error) {
+ console.error(error);
+ throw error;
+ } finally {
+ this.setLoading(false);
+ }
}
- public logout() {}
+ public async logout() {
+ try {
+ this.setLoading(true);
+ const result = await this.root.authService.requestLogout();
+
+ this.setIsLoggedIn(false);
+ return result;
+ } catch (error) {
+ console.error(error);
+ throw error;
+ } finally {
+ this.setLoading(false);
+ }
+ }
}
diff --git a/src/stores/RootStore.ts b/src/stores/RootStore.ts
index 3b21813..d0b0d73 100644
--- a/src/stores/RootStore.ts
+++ b/src/stores/RootStore.ts
@@ -3,10 +3,16 @@ import AuthStore from './AuthStore';
import ApiService from '../services/ApiService';
import AuthService from '../services/AuthService';
+import UserStore from './UserStore';
+import { makeAutoObservable } from 'mobx';
export default class RootStore {
+ public networkApiError: any;
+
public authStore: AuthStore;
+ public userStore: UserStore;
+
public apiService: ApiService;
public authService: AuthService;
@@ -16,6 +22,7 @@ export default class RootStore {
* Stores
*/
this.authStore = new AuthStore(this);
+ this.userStore = new UserStore(this);
/**
* Services
@@ -23,7 +30,13 @@ export default class RootStore {
this.apiService = new ApiService(this, {
baseURL: API_BASE_URL,
responseType: 'json',
+ withCredentials: true,
});
- this.authService = new AuthService(this.apiService);
+ this.authService = new AuthService(this.apiService.api);
+ makeAutoObservable(this);
+ }
+
+ public setNetworkApiError(error: any) {
+ this.networkApiError = error;
}
}
diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts
new file mode 100644
index 0000000..12ff672
--- /dev/null
+++ b/src/stores/UserStore.ts
@@ -0,0 +1,20 @@
+import { makeAutoObservable } from 'mobx';
+import RootStore from './RootStore';
+
+export interface IUser {
+ id: string;
+ first_name: string;
+ last_name: string;
+ email: string;
+}
+
+export default class UserStore {
+ public root: RootStore;
+
+ public user: IUser | null = null;
+
+ constructor(root: RootStore) {
+ this.root = root;
+ makeAutoObservable(this);
+ }
+}