diff --git a/package-lock.json b/package-lock.json
index db8ec2a..6bebd30 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2819,6 +2819,12 @@
}
}
},
+ "@types/history": {
+ "version": "4.7.9",
+ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
+ "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
+ "dev": true
+ },
"@types/html-minifier-terser": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz",
@@ -2845,6 +2851,12 @@
"@types/istanbul-lib-report": "*"
}
},
+ "@types/js-cookie": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
+ "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==",
+ "dev": true
+ },
"@types/json-schema": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@@ -2924,6 +2936,36 @@
}
}
},
+ "@types/react-helmet": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.2.tgz",
+ "integrity": "sha512-dcfAZNlWb5JYFbO9CGfrPWLJAyFcT6UeR3u35eBbv8liY2Rg4K7fM1G5+HnwVgot+C+kVwXAZ8pLEn2jsMfTDg==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/react-router": {
+ "version": "5.1.16",
+ "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz",
+ "integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==",
+ "dev": true,
+ "requires": {
+ "@types/history": "*",
+ "@types/react": "*"
+ }
+ },
+ "@types/react-router-dom": {
+ "version": "5.1.8",
+ "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz",
+ "integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==",
+ "dev": true,
+ "requires": {
+ "@types/history": "*",
+ "@types/react": "*",
+ "@types/react-router": "*"
+ }
+ },
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
@@ -3763,6 +3805,14 @@
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.2.tgz",
"integrity": "sha512-5LMaDRWm8ZFPAEdzTYmgjjEdj1YnQcpfrVajO/sn/LhbpGp0Y0H64c2hLZI1gRMxfA+w1S71Uc/nHaOXgcCvGg=="
},
+ "axios": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
+ "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
+ "requires": {
+ "follow-redirects": "^1.10.0"
+ }
+ },
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -9493,6 +9543,11 @@
}
}
},
+ "js-cookie": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.0.tgz",
+ "integrity": "sha512-oUbbplKuH07/XX2YD2+Q+GMiPpnVXaRz8npE7suhBH9QEkJe2W7mQ6rwuMXHue3fpfcftQwzgyvGzIHyfCSngQ=="
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -10121,6 +10176,24 @@
"minimist": "^1.2.5"
}
},
+ "mobx": {
+ "version": "6.3.2",
+ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.3.2.tgz",
+ "integrity": "sha512-xGPM9dIE1qkK9Nrhevp0gzpsmELKU4MFUJRORW/jqxVFIHHWIoQrjDjL8vkwoJYY3C2CeVJqgvl38hgKTalTWg=="
+ },
+ "mobx-react": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-7.2.0.tgz",
+ "integrity": "sha512-KHUjZ3HBmZlNnPd1M82jcdVsQRDlfym38zJhZEs33VxyVQTvL77hODCArq6+C1P1k/6erEeo2R7rpE7ZeOL7dg==",
+ "requires": {
+ "mobx-react-lite": "^3.2.0"
+ }
+ },
+ "mobx-react-lite": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz",
+ "integrity": "sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g=="
+ },
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
diff --git a/package.json b/package.json
index 7ee6078..f09465f 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,11 @@
"@types/jest": "^25.0.0",
"@types/node": "^12.0.0",
"@types/react-dom": "^16.9.0",
+ "axios": "^0.21.1",
"framer-motion": "^4.0.0",
+ "js-cookie": "^3.0.0",
+ "mobx": "^6.3.2",
+ "mobx-react": "^7.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
@@ -47,6 +51,9 @@
]
},
"devDependencies": {
- "@types/react": "^16.14.13"
+ "@types/js-cookie": "^2.2.7",
+ "@types/react": "^16.14.13",
+ "@types/react-helmet": "^6.1.2",
+ "@types/react-router-dom": "^5.1.8"
}
}
diff --git a/src/App.tsx b/src/App.tsx
index a896318..f3bee20 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,14 +1,21 @@
-import * as React from "react";
-import { ChakraProvider, Box, Text, Grid, theme } from "@chakra-ui/react";
-import { ColorModeSwitcher } from "./ColorModeSwitcher";
+import * as 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';
+
+/**
+ * Pages
+ */
+import MainPage from './pages/main';
+import AuthPage from './pages/auth';
export const App = () => (
-
-
-
- Appliction
-
-
+
+
+
+
+
+
);
diff --git a/src/axios/index.ts b/src/axios/index.ts
new file mode 100644
index 0000000..e792de1
--- /dev/null
+++ b/src/axios/index.ts
@@ -0,0 +1,10 @@
+import axios from 'axios';
+import { API_BASE_URL } from '../constants';
+
+/**
+ * Create axios config
+ */
+export default axios.create({
+ baseURL: API_BASE_URL,
+ responseType: 'json',
+});
diff --git a/src/components/Auth/Auth.tsx b/src/components/Auth/Auth.tsx
new file mode 100644
index 0000000..924bcf2
--- /dev/null
+++ b/src/components/Auth/Auth.tsx
@@ -0,0 +1,104 @@
+import React, { useState } from 'react';
+import { MdAccountCircle, MdVpnKey } from 'react-icons/md';
+import {
+ Flex,
+ Box,
+ FormControl,
+ FormLabel,
+ InputGroup,
+ Input,
+ InputLeftElement,
+ Button,
+ Icon,
+} from '@chakra-ui/react';
+import { useRootStore } from '../../hooks/useRootStore';
+import { useHistory } from 'react-router-dom';
+import { observer } from 'mobx-react';
+
+type LoginDataInput = {
+ login: string;
+ password: string;
+};
+
+/**
+ * Authorize user through a form.
+ */
+const Auth: React.FC = () => {
+ const [loginData, setLoginData] = useState({
+ login: '',
+ password: '',
+ });
+ const { authStore } = useRootStore();
+ const history = useHistory();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const { login, password } = loginData;
+ await authStore.login(login, password);
+ history.push('/');
+ };
+
+ return (
+
+
+
+
+ {/* */}
+
+
+
+
+
+
+
+ );
+};
+
+export default observer(Auth);
diff --git a/src/ColorModeSwitcher.tsx b/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx
similarity index 100%
rename from src/ColorModeSwitcher.tsx
rename to src/components/ColorModeSwitcher/ColorModeSwitcher.tsx
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
new file mode 100644
index 0000000..320dd80
--- /dev/null
+++ b/src/components/Layout/Layout.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Flex, Box } from '@chakra-ui/react';
+
+/**
+ *
+ * Layout component
+ * Сomponent for rendering the current page.
+ */
+const Layout: React.FC = ({ children }) => {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export default Layout;
diff --git a/src/components/PrivateRoute/PrivateRoute.tsx b/src/components/PrivateRoute/PrivateRoute.tsx
new file mode 100644
index 0000000..d590aa0
--- /dev/null
+++ b/src/components/PrivateRoute/PrivateRoute.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import Helmet from 'react-helmet';
+import Layout from '../Layout/Layout';
+import { Route, Redirect } from 'react-router-dom';
+import { IPrivateRouteProps } from '../../interfaces';
+import { useRootStore } from '../../hooks/useRootStore';
+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 { authStore } = useRootStore();
+ const isLoggedIn = authStore.isLoggedIn;
+
+ return (
+
+ isLoggedIn ? (
+
+
+ {title} • App
+
+
+
+ ) : (
+
+ )
+ }
+ />
+ );
+};
+
+export default observer(PrivateRoute);
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..120b1eb
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1 @@
+export const API_BASE_URL: string = 'http://localhost:3333/api';
diff --git a/src/contexts/index.ts b/src/contexts/index.ts
new file mode 100644
index 0000000..403067e
--- /dev/null
+++ b/src/contexts/index.ts
@@ -0,0 +1,6 @@
+import React from 'react';
+import RootStore from '../stores/RootStore';
+
+export const StoreContext = React.createContext(
+ undefined
+);
diff --git a/src/hooks/useRootStore.ts b/src/hooks/useRootStore.ts
new file mode 100644
index 0000000..dc8750c
--- /dev/null
+++ b/src/hooks/useRootStore.ts
@@ -0,0 +1,11 @@
+import React from 'react';
+import { StoreContext } from '../contexts';
+
+export const useRootStore = () => {
+ const context = React.useContext(StoreContext);
+ if (context === undefined) {
+ throw new Error('useRootStore must be used within RootStoreProvider');
+ }
+
+ return context;
+};
diff --git a/src/index.tsx b/src/index.tsx
index 586fc62..c795ec6 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,15 +1,18 @@
-import { ColorModeScript } from "@chakra-ui/react";
-import * as React from "react";
-import ReactDOM from "react-dom";
-import { App } from "./App";
-import reportWebVitals from "./reportWebVitals";
+import { ColorModeScript } from '@chakra-ui/react';
+import * as React from 'react';
+import ReactDOM from 'react-dom';
+import { App } from './App';
+import { RootStoreProvider } from './providers';
+import reportWebVitals from './reportWebVitals';
ReactDOM.render(
-
+ <>
-
- ,
- document.getElementById("root")
+
+
+
+ >,
+ document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts
new file mode 100644
index 0000000..651050e
--- /dev/null
+++ b/src/interfaces/index.ts
@@ -0,0 +1,17 @@
+import type { RouteProps } from 'react-router-dom';
+import type { RouteComponentProps } from 'react-router-dom';
+
+export interface IDataResult {
+ data: any;
+ message: string;
+ status: number;
+}
+
+export interface IPageProps extends RouteComponentProps {
+ title?: string;
+}
+
+export interface IPrivateRouteProps extends RouteProps {
+ component: React.FC;
+ title?: string;
+}
diff --git a/src/pages/auth.tsx b/src/pages/auth.tsx
new file mode 100644
index 0000000..9755732
--- /dev/null
+++ b/src/pages/auth.tsx
@@ -0,0 +1,30 @@
+import { observer } from 'mobx-react';
+import React from 'react';
+import Helmet from 'react-helmet';
+import { useHistory } from 'react-router-dom';
+import Auth from '../components/Auth/Auth';
+import { useRootStore } from '../hooks/useRootStore';
+import { IPageProps } from '../interfaces';
+
+/**
+ * Auth page
+ */
+const AuthPage: React.FC = () => {
+ const { authStore } = useRootStore();
+ const history = useHistory();
+
+ if (authStore.isLoggedIn) {
+ history.push('/');
+ }
+
+ return (
+ <>
+
+ Auth
+
+
+ >
+ );
+};
+
+export default observer(AuthPage);
diff --git a/src/pages/main.tsx b/src/pages/main.tsx
new file mode 100644
index 0000000..217cb08
--- /dev/null
+++ b/src/pages/main.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import {
+ Box,
+ Heading,
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ Grid,
+} from '@chakra-ui/react';
+import { IPageProps } from '../interfaces';
+
+/**
+ * Main page
+ */
+const MainPage: React.FC = ({ title, history }) => {
+ return (
+ <>
+
+
+ {title}
+
+
+
+
+ Main page
+
+ ...
+
+
+
+ >
+ );
+};
+
+export default MainPage;
diff --git a/src/providers/index.tsx b/src/providers/index.tsx
new file mode 100644
index 0000000..1bbf1ed
--- /dev/null
+++ b/src/providers/index.tsx
@@ -0,0 +1,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};
+};
diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts
new file mode 100644
index 0000000..13d01b7
--- /dev/null
+++ b/src/services/AuthService.ts
@@ -0,0 +1,24 @@
+import api from '../axios/index';
+import { IDataResult } from '../interfaces';
+
+interface ILoginResult extends IDataResult {
+ data: {
+ token: string;
+ type: string;
+ expires_at: string;
+ };
+}
+
+export default class AuthService {
+ public async requestLogin(
+ login: string,
+ password: string
+ ): Promise {
+ const result = await api.post('v1/auth/login', {
+ login,
+ password,
+ });
+
+ return result.data;
+ }
+}
diff --git a/src/stores/AuthStore.ts b/src/stores/AuthStore.ts
new file mode 100644
index 0000000..5a95621
--- /dev/null
+++ b/src/stores/AuthStore.ts
@@ -0,0 +1,42 @@
+import { makeAutoObservable } from 'mobx';
+import Cookies from 'js-cookie';
+import AuthService from '../services/AuthService';
+import RootStore from './RootStore';
+
+export default class AuthStore {
+ public root: RootStore;
+
+ public token: string | null = null;
+
+ public isLoggedIn: boolean = !!Cookies.get('logged_in');
+
+ public authService: AuthService;
+
+ constructor(root: RootStore) {
+ this.root = root;
+ this.authService = new AuthService();
+ makeAutoObservable(this);
+ }
+
+ get getToken(): string | null {
+ return this.token;
+ }
+
+ public setIsLoggedIn(value: boolean) {
+ this.isLoggedIn = value;
+ }
+
+ public setToken(token: string) {
+ Cookies.set('logged_in', 'true');
+ this.token = token;
+ }
+
+ public async login(login: string, password: string) {
+ const result = await this.authService.requestLogin(login, password);
+
+ this.setIsLoggedIn(true);
+ this.setToken(result.data.token);
+ }
+
+ public logout() {}
+}
diff --git a/src/stores/RootStore.ts b/src/stores/RootStore.ts
new file mode 100644
index 0000000..b58e4b4
--- /dev/null
+++ b/src/stores/RootStore.ts
@@ -0,0 +1,9 @@
+import AuthStore from './AuthStore';
+
+export default class RootStore {
+ public authStore: AuthStore;
+
+ constructor() {
+ this.authStore = new AuthStore(this);
+ }
+}