feat: stores

This commit is contained in:
Sergey Yarkov 2021-08-18 23:03:33 +03:00
parent 2b655ddd09
commit 73841e8f68
19 changed files with 483 additions and 19 deletions

73
package-lock.json generated
View File

@ -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": { "@types/html-minifier-terser": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", "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/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": { "@types/json-schema": {
"version": "7.0.9", "version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", "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": { "@types/resolve": {
"version": "0.0.8", "version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", "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", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.2.tgz",
"integrity": "sha512-5LMaDRWm8ZFPAEdzTYmgjjEdj1YnQcpfrVajO/sn/LhbpGp0Y0H64c2hLZI1gRMxfA+w1S71Uc/nHaOXgcCvGg==" "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": { "axobject-query": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", "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": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -10121,6 +10176,24 @@
"minimist": "^1.2.5" "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": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",

View File

@ -12,7 +12,11 @@
"@types/jest": "^25.0.0", "@types/jest": "^25.0.0",
"@types/node": "^12.0.0", "@types/node": "^12.0.0",
"@types/react-dom": "^16.9.0", "@types/react-dom": "^16.9.0",
"axios": "^0.21.1",
"framer-motion": "^4.0.0", "framer-motion": "^4.0.0",
"js-cookie": "^3.0.0",
"mobx": "^6.3.2",
"mobx-react": "^7.2.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
@ -47,6 +51,9 @@
] ]
}, },
"devDependencies": { "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"
} }
} }

View File

@ -1,14 +1,21 @@
import * as React from "react"; import * as React from 'react';
import { ChakraProvider, Box, Text, Grid, theme } from "@chakra-ui/react"; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { ColorModeSwitcher } from "./ColorModeSwitcher"; 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 = () => ( export const App = () => (
<ChakraProvider theme={theme}> <ChakraProvider theme={theme}>
<Box textAlign="center" fontSize="xl"> <Router>
<Grid minH="100vh" p={3}> <Switch>
<ColorModeSwitcher justifySelf="flex-end" /> <PrivateRoute exact path='/' component={MainPage} title='Main' />
<Text>Appliction</Text> <Route path='/auth' component={AuthPage} />
</Grid> </Switch>
</Box> </Router>
</ChakraProvider> </ChakraProvider>
); );

10
src/axios/index.ts Normal file
View File

@ -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',
});

View File

@ -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<LoginDataInput>({
login: '',
password: '',
});
const { authStore } = useRootStore();
const history = useHistory();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const { login, password } = loginData;
await authStore.login(login, password);
history.push('/');
};
return (
<Flex minHeight='100vh' align='center' justifyContent='center'>
<Box
p={8}
my={40}
width='full'
maxWidth='450px'
borderWidth={1}
borderRadius={8}>
<Box textAlign='center'>
<Flex justifyContent='center'>
{/* <LogoIcon viewBox='0 0 24 24' boxSize={'64px'} fill='blue.600' /> */}
</Flex>
</Box>
<Box my='30px'>
<form onSubmit={handleSubmit}>
<FormControl isRequired={true}>
<FormLabel>Login</FormLabel>
<InputGroup>
<InputLeftElement children={<Icon as={MdAccountCircle} />} />
<Input
type='text'
name='login'
placeholder='email@mail.com'
value={loginData.login}
onChange={(e) =>
setLoginData({ ...loginData, login: e.target.value })
}
/>
</InputGroup>
</FormControl>
<FormControl mt={5} isRequired={true}>
<FormLabel>Password</FormLabel>
<InputGroup>
<InputLeftElement children={<Icon as={MdVpnKey} />} />
<Input
type='password'
name='password'
placeholder='********'
value={loginData.password}
onChange={(e) =>
setLoginData({ ...loginData, password: e.target.value })
}
/>
</InputGroup>
</FormControl>
<Button
loadingText='Logging in...'
type='submit'
colorScheme='blue'
variant='outline'
width='full'
mt={4}>
Log in
</Button>
</form>
</Box>
</Box>
</Flex>
);
};
export default observer(Auth);

View File

@ -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 (
<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'>
{children}
</Box>
</Box>
</Flex>
);
};
export default Layout;

View File

@ -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<IPrivateRouteProps> = ({
children,
component: Component,
title,
...options
}) => {
const { authStore } = useRootStore();
const isLoggedIn = authStore.isLoggedIn;
return (
<Route
{...options}
render={(props) =>
isLoggedIn ? (
<Layout>
<Helmet>
<title>{title} • App </title>
</Helmet>
<Component {...props} title={title} />
</Layout>
) : (
<Redirect
to={{ pathname: '/auth', state: { from: props.location } }}
/>
)
}
/>
);
};
export default observer(PrivateRoute);

1
src/constants.ts Normal file
View File

@ -0,0 +1 @@
export const API_BASE_URL: string = 'http://localhost:3333/api';

6
src/contexts/index.ts Normal file
View File

@ -0,0 +1,6 @@
import React from 'react';
import RootStore from '../stores/RootStore';
export const StoreContext = React.createContext<RootStore | undefined>(
undefined
);

11
src/hooks/useRootStore.ts Normal file
View File

@ -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;
};

View File

@ -1,15 +1,18 @@
import { ColorModeScript } from "@chakra-ui/react"; import { ColorModeScript } from '@chakra-ui/react';
import * as React from "react"; import * as React from 'react';
import ReactDOM from "react-dom"; import ReactDOM from 'react-dom';
import { App } from "./App"; import { App } from './App';
import reportWebVitals from "./reportWebVitals"; import { RootStoreProvider } from './providers';
import reportWebVitals from './reportWebVitals';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <>
<ColorModeScript /> <ColorModeScript />
<App /> <RootStoreProvider>
</React.StrictMode>, <App />
document.getElementById("root") </RootStoreProvider>
</>,
document.getElementById('root')
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

17
src/interfaces/index.ts Normal file
View File

@ -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<any>;
title?: string;
}

30
src/pages/auth.tsx Normal file
View File

@ -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<IPageProps> = () => {
const { authStore } = useRootStore();
const history = useHistory();
if (authStore.isLoggedIn) {
history.push('/');
}
return (
<>
<Helmet>
<title>Auth</title>
</Helmet>
<Auth />
</>
);
};
export default observer(AuthPage);

35
src/pages/main.tsx Normal file
View File

@ -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<IPageProps> = ({ title, history }) => {
return (
<>
<Breadcrumb fontWeight='medium' fontSize='sm'>
<BreadcrumbItem isCurrentPage>
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
</BreadcrumbItem>
</Breadcrumb>
<Box marginTop={10}>
<Box>
<Heading as='h1'>Main page</Heading>
<Grid templateColumns='3fr 1fr' mt='25px'>
...
</Grid>
</Box>
</Box>
</>
);
};
export default MainPage;

13
src/providers/index.tsx Normal file
View File

@ -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 <StoreContext.Provider value={root}>{children}</StoreContext.Provider>;
};

View File

@ -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<ILoginResult> {
const result = await api.post('v1/auth/login', {
login,
password,
});
return result.data;
}
}

42
src/stores/AuthStore.ts Normal file
View File

@ -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() {}
}

9
src/stores/RootStore.ts Normal file
View File

@ -0,0 +1,9 @@
import AuthStore from './AuthStore';
export default class RootStore {
public authStore: AuthStore;
constructor() {
this.authStore = new AuthStore(this);
}
}