diff --git a/package.json b/package.json
index c79622c..bd60e24 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,9 @@
"@testing-library/user-event": "^12.1.10",
"@zxing/library": "^0.18.3",
"add": "^2.0.6",
+ "connected-react-router": "^6.8.0",
+ "history": "4.10.1",
+ "immer": "^8.0.0",
"lodash": "^4.17.20",
"prop-types": "^15.7.2",
"react": "^17.0.1",
@@ -21,6 +24,7 @@
"react-webcam": "^5.2.2",
"recharts": "^1.8.5",
"redux": "^4.0.5",
+ "redux-injectors": "^1.3.0",
"redux-saga": "^1.1.3",
"reselect": "^4.0.0",
"web-vitals": "^0.2.4",
diff --git a/src/containers/App/actions.js b/src/containers/App/actions.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/containers/App/constants.js b/src/containers/App/constants.js
new file mode 100644
index 0000000..755d49d
--- /dev/null
+++ b/src/containers/App/constants.js
@@ -0,0 +1,4 @@
+export const LOGOUT_REQUEST = 'app/App/LOGOUT_REQUEST';
+export const LOGOUT_ERROR = 'app/App/LOGOUT_ERROR';
+export const LOGOUT_SUCCESS = 'app/App/LOGOUT_SUCCESS';
+export const LOGIN_SUCCESS = 'app/App/LOGIN_SUCCESS';
diff --git a/src/containers/App/index.js b/src/containers/App/index.js
index c29af36..dfc6eda 100644
--- a/src/containers/App/index.js
+++ b/src/containers/App/index.js
@@ -1,28 +1,30 @@
import React from 'react';
-import { Switch } from 'react-router-dom';
-import RouteWithSubRoutes from 'containers/RouteWithSubRoutes'
+import {Route, Switch} from 'react-router-dom';
import { ThemeProvider } from '@material-ui/core/styles'
+import {createStructuredSelector} from "reselect";
import CssBaseline from '@material-ui/core/CssBaseline'
-import { ROUTES } from 'utils/routes';
-import { theme } from 'utils/theme'
-
+import { theme, routes } from 'utils'
+import {makeSelectIsLogged} from "./selectors";
+import { useSelector } from 'react-redux';
import Navbar from 'components/Navbar'
+import HomePage from 'pages/Home'
+import LoginPage from 'pages/Login'
+
+const stateSelector = createStructuredSelector({
+ isLogged: makeSelectIsLogged(),
+});
+
const App = () => {
+ const { isLogged } = useSelector(stateSelector)
+
return (
-
+ {isLogged && }
- {ROUTES.map(({ key, component, path, exact, routes }) => (
-
- ))}
+
+
);
diff --git a/src/containers/App/reducer.js b/src/containers/App/reducer.js
new file mode 100644
index 0000000..72e87fc
--- /dev/null
+++ b/src/containers/App/reducer.js
@@ -0,0 +1,30 @@
+import produce from 'immer';
+import {
+ LOGOUT_ERROR,
+ LOGOUT_SUCCESS,
+} from './constants';
+
+import { LOGIN_SUCCESS } from 'pages/Login/constants'
+
+export const initialState = {
+ isLogged: false,
+ notifications: [],
+ tokens: {},
+ user: {},
+};
+const appReducer = produce((draft, action) => {
+ switch(action.type) {
+ case LOGIN_SUCCESS:
+ draft.isLogged = true;
+ draft.tokens = action.tokens;
+ draft.user = action.user;
+ break;
+ case LOGOUT_ERROR:
+ case LOGOUT_SUCCESS:
+ return initialState;
+ default:
+ return initialState;
+ }
+}, initialState);
+
+export default appReducer;
diff --git a/src/containers/App/selectors.js b/src/containers/App/selectors.js
new file mode 100644
index 0000000..81ef201
--- /dev/null
+++ b/src/containers/App/selectors.js
@@ -0,0 +1,23 @@
+import { createSelector } from 'reselect';
+import { initialState } from './reducer';
+
+const selectGlobalDomain = (state) => state.global || initialState;
+
+const makeSelectIsLogged = () =>
+ createSelector(selectGlobalDomain, (substate) => substate.isLogged);
+
+const makeSelectNotifications = () =>
+ createSelector(selectGlobalDomain, (substate) => substate.notifications);
+
+const makeSelectTokens = () =>
+ createSelector(selectGlobalDomain, (substate) => substate.tokens);
+
+const makeSelectUser = () =>
+ createSelector(selectGlobalDomain, (substate) => substate.user);
+
+export {
+ makeSelectIsLogged,
+ makeSelectNotifications,
+ makeSelectTokens,
+ makeSelectUser,
+};
diff --git a/src/containers/RouteWithSubRoutes/index.js b/src/containers/RouteWithSubRoutes/index.js
deleted file mode 100644
index 9df255f..0000000
--- a/src/containers/RouteWithSubRoutes/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import { Route } from 'react-router-dom'
-import PropTypes from 'prop-types';
-
-const RouteWithSubRoutes = ({ component: Component, path, exact, routes = [] }) => (
- }
- />
-)
-
-RouteWithSubRoutes.propTypes = {
- routes: PropTypes.array,
- component: PropTypes.elementType.isRequired,
- path: PropTypes.string.isRequired,
- exact: PropTypes.bool.isRequired,
-}
-
-export default RouteWithSubRoutes;
diff --git a/src/index.js b/src/index.js
index 967621a..ecfe17f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,14 +1,22 @@
import React from 'react';
import ReactDOM from 'react-dom';
-import { BrowserRouter } from 'react-router-dom'
+import { Provider } from 'react-redux';
+import { ConnectedRouter } from 'connected-react-router';
import App from 'containers/App';
import reportWebVitals from './reportWebVitals';
+import configureStore from 'utils/configureStore'
+import {history} from 'utils';
+
+const initialState = {};
+const store = configureStore(initialState, history);
ReactDOM.render(
-
-
-
+
+
+
+
+
,
document.getElementById('root')
);
diff --git a/src/pages/Login/actions.js b/src/pages/Login/actions.js
new file mode 100644
index 0000000..1e8db07
--- /dev/null
+++ b/src/pages/Login/actions.js
@@ -0,0 +1,22 @@
+import { LOGIN_INPUT_CHANGE, LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_ERROR } from './constants';
+
+export const loginInputChange = (name, value) => ({
+ type: LOGIN_INPUT_CHANGE,
+ name,
+ value,
+})
+
+export const loginAction = () => ({
+ type: LOGIN_REQUEST,
+})
+
+export const loginSuccessAction = (user, tokens) => ({
+ type: LOGIN_SUCCESS,
+ user,
+ tokens,
+})
+
+export const loginErrorAction = (error) => ({
+ type: LOGIN_ERROR,
+ error,
+})
diff --git a/src/pages/Login/constants.js b/src/pages/Login/constants.js
new file mode 100644
index 0000000..e7f2f4c
--- /dev/null
+++ b/src/pages/Login/constants.js
@@ -0,0 +1,5 @@
+export const LOGIN_REQUEST = 'app/LoginPage/LOGIN_REQUEST';
+export const LOGIN_SUCCESS = 'app/LoginPage/LOGIN_SUCCESS';
+export const LOGIN_ERROR = 'app/LoginPage/LOGIN_ERROR';
+
+export const LOGIN_INPUT_CHANGE = 'app/LoginPage/LOGIN_INPUT_CHANGE';
diff --git a/src/pages/Login/index.js b/src/pages/Login/index.js
new file mode 100644
index 0000000..2705052
--- /dev/null
+++ b/src/pages/Login/index.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import { useInjectReducer, useInjectSaga } from 'redux-injectors';
+import { createStructuredSelector } from 'reselect';
+import { useSelector, useDispatch } from 'react-redux';
+import { Container, Grid, Box, Paper, Button, Typography, TextField } from '@material-ui/core'
+import reducer from './reducer';
+import saga from './saga';
+import { makeSelectEmail, makeSelectPassword, makeSelectLoading } from './selectors'
+import { loginInputChange, loginAction } from './actions'
+
+const stateSelector = createStructuredSelector({
+ email: makeSelectEmail(),
+ password: makeSelectPassword(),
+ loading: makeSelectLoading(),
+});
+
+const key = 'loginPage'
+const Login = () => {
+ useInjectReducer({ key, reducer });
+ useInjectSaga({ key, saga });
+ const { email, password, loading } = useSelector(stateSelector)
+ const dispatch = useDispatch();
+
+ const onChangeInput = ({target: { name, value }}) => {
+ dispatch(loginInputChange(name, value))
+ }
+
+ const handleSubmit = (event) => {
+ event.preventDefault()
+ dispatch(loginAction())
+ }
+
+ return (
+
+
+
+
+ Login
+
+
+
+
+
+
+
+ );
+};
+
+export default Login;
diff --git a/src/pages/Login/reducer.js b/src/pages/Login/reducer.js
new file mode 100644
index 0000000..760add0
--- /dev/null
+++ b/src/pages/Login/reducer.js
@@ -0,0 +1,31 @@
+import produce from 'immer';
+import {LOGIN_INPUT_CHANGE, LOGIN_ERROR, LOGIN_REQUEST, LOGIN_SUCCESS} from './constants';
+
+export const initialState = {
+ loading: false,
+ error: {},
+ email: 'admin@admin.com',
+ password: 'Kox32113@#$',
+};
+
+const loginPageReducer = produce((draft, action) => {
+ switch(action.type) {
+ case LOGIN_SUCCESS:
+ draft.loading = false;
+ break;
+ case LOGIN_REQUEST:
+ draft.loading = true;
+ break;
+ case LOGIN_ERROR:
+ draft.loading = false;
+ draft.error = action.error;
+ break;
+ case LOGIN_INPUT_CHANGE:
+ draft[action.name] = action.value;
+ break;
+ default:
+ return initialState;
+ }
+}, initialState);
+
+export default loginPageReducer;
diff --git a/src/pages/Login/saga.js b/src/pages/Login/saga.js
new file mode 100644
index 0000000..1cfc3d6
--- /dev/null
+++ b/src/pages/Login/saga.js
@@ -0,0 +1,30 @@
+import { takeLatest, call, put, select } from 'redux-saga/effects';
+import { push } from 'connected-react-router';
+import { api, request, routes } from 'utils';
+import { LOGIN_REQUEST } from './constants';
+import { makeSelectPassword, makeSelectEmail } from './selectors';
+import { loginSuccessAction, loginErrorAction } from './actions';
+
+export function* login() {
+ const email = yield select(makeSelectEmail());
+ const password = yield select(makeSelectPassword());
+
+ const requestURL = api.auth.login;
+ const requestParameters = {
+ method: 'POST',
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password }),
+ };
+
+ try {
+ const { tokens, user } = yield call(request, requestURL, requestParameters);
+ yield put(loginSuccessAction(user, tokens));
+ yield put(push(routes.dashboard.path));
+ } catch (error) {
+ yield put(loginErrorAction(error.message));
+ }
+}
+
+export default function* loginPageSaga() {
+ yield takeLatest(LOGIN_REQUEST, login);
+}
diff --git a/src/pages/Login/selectors.js b/src/pages/Login/selectors.js
new file mode 100644
index 0000000..0d66bc1
--- /dev/null
+++ b/src/pages/Login/selectors.js
@@ -0,0 +1,19 @@
+import { createSelector } from 'reselect';
+import { initialState } from './reducer';
+
+const selectLoginPageDomain = (state) => state.loginPage || initialState;
+
+const makeSelectEmail = () =>
+ createSelector(selectLoginPageDomain, (substate) => substate.email);
+
+const makeSelectPassword = () =>
+ createSelector(selectLoginPageDomain, (substate) => substate.password);
+const makeSelectLoading = () =>
+ createSelector(selectLoginPageDomain, (substate) => substate.loading);
+
+export {
+ selectLoginPageDomain,
+ makeSelectEmail,
+ makeSelectPassword,
+ makeSelectLoading,
+};
diff --git a/src/utils/api.js b/src/utils/api.js
new file mode 100644
index 0000000..dfaa36a
--- /dev/null
+++ b/src/utils/api.js
@@ -0,0 +1,11 @@
+const API_BASE_URL = 'http://localhost:3001/v1'
+const AUTH = 'auth';
+
+const urls = {
+ auth: {
+ login: `${API_BASE_URL}/${AUTH}/login`,
+ register: `${API_BASE_URL}/${AUTH}/register`,
+ }
+}
+
+export default urls
diff --git a/src/utils/configureStore.js b/src/utils/configureStore.js
new file mode 100644
index 0000000..5db2f3f
--- /dev/null
+++ b/src/utils/configureStore.js
@@ -0,0 +1,41 @@
+import { createStore, applyMiddleware, compose } from 'redux';
+import { routerMiddleware } from 'connected-react-router';
+import { createInjectorsEnhancer } from 'redux-injectors';
+import createSagaMiddleware from 'redux-saga';
+import createReducer from './reducers';
+
+export default function configureStore(initialState = {}, history) {
+ let composeEnhancers = compose;
+ const reduxSagaMonitorOptions = {};
+
+ if (typeof window === 'object') {
+ /* eslint-disable no-underscore-dangle */
+ if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__)
+ composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({});
+
+ }
+
+ const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);
+ const { run: runSaga } = sagaMiddleware;
+
+ // Create the store with two middlewares
+ // 1. sagaMiddleware: Makes redux-sagas work
+ // 2. routerMiddleware: Syncs the location/URL path to the state
+ const middlewares = [sagaMiddleware, routerMiddleware(history)];
+
+ const enhancers = [
+ applyMiddleware(...middlewares),
+ createInjectorsEnhancer({
+ createReducer,
+ runSaga,
+ }),
+ ];
+
+ const store = createStore(
+ createReducer(),
+ initialState,
+ composeEnhancers(...enhancers),
+ );
+
+ return store;
+}
diff --git a/src/utils/history.js b/src/utils/history.js
new file mode 100644
index 0000000..ee3abb7
--- /dev/null
+++ b/src/utils/history.js
@@ -0,0 +1,3 @@
+import { createBrowserHistory } from 'history';
+const history = createBrowserHistory();
+export default history;
diff --git a/src/utils/index.js b/src/utils/index.js
new file mode 100644
index 0000000..3708272
--- /dev/null
+++ b/src/utils/index.js
@@ -0,0 +1,5 @@
+export { default as api } from './api';
+export { default as request } from './request';
+export { default as routes } from './routes';
+export { default as history } from './history';
+export { default as theme } from './theme';
diff --git a/src/utils/reducers.js b/src/utils/reducers.js
new file mode 100644
index 0000000..dfcfdfd
--- /dev/null
+++ b/src/utils/reducers.js
@@ -0,0 +1,15 @@
+import { combineReducers } from 'redux';
+import { connectRouter } from 'connected-react-router';
+
+import {history} from 'utils';
+import globalReducer from 'containers/App/reducer';
+
+export default function createReducer(injectedReducers = {}) {
+ const rootReducer = combineReducers({
+ global: globalReducer,
+ router: connectRouter(history),
+ ...injectedReducers,
+ });
+
+ return rootReducer;
+}
diff --git a/src/utils/request.js b/src/utils/request.js
new file mode 100644
index 0000000..8690992
--- /dev/null
+++ b/src/utils/request.js
@@ -0,0 +1,24 @@
+function parseJSON(response) {
+ if (response.status === 204 || response.status === 205) {
+ return null;
+ }
+ return response.json();
+}
+
+function checkStatus(response) {
+ if (response.status >= 200 && response.status < 300) {
+ return response;
+ }
+
+ const error = new Error(response.statusText);
+ error.statusCode = response.status;
+ throw error;
+}
+
+const request = async (url, options) => {
+ const fetchResponse = await fetch(url, options);
+ const response = await checkStatus(fetchResponse);
+ return parseJSON(response);
+}
+
+export default request
diff --git a/src/utils/routes.js b/src/utils/routes.js
index 44b52d8..53b4593 100644
--- a/src/utils/routes.js
+++ b/src/utils/routes.js
@@ -1,10 +1,13 @@
-import HomePage from 'pages/Home'
-export const ROUTES = [
- {
+const routes = {
+ dashboard: {
path: '/',
- key: 'HOME',
exact: true,
- component: HomePage
},
-]
+ login: {
+ path: '/login',
+ exact: true,
+ },
+}
+
+export default routes
diff --git a/src/utils/theme.js b/src/utils/theme.js
index 606bd8c..72492e6 100644
--- a/src/utils/theme.js
+++ b/src/utils/theme.js
@@ -1,6 +1,6 @@
import { createMuiTheme } from '@material-ui/core/styles';
-export const theme = createMuiTheme({
+const theme = createMuiTheme({
palette: {
type: 'dark',
primary: {
@@ -11,3 +11,4 @@ export const theme = createMuiTheme({
},
},
})
+export default theme
diff --git a/yarn.lock b/yarn.lock
index ca58d2e..9bcd675 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3466,6 +3466,13 @@ connect-history-api-fallback@^1.6.0:
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
+connected-react-router@^6.8.0:
+ version "6.8.0"
+ resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.8.0.tgz#ddc687b31d498322445d235d660798489fa56cae"
+ integrity sha512-E64/6krdJM3Ag3MMmh2nKPtMbH15s3JQDuaYJvOVXzu6MbHbDyIvuwLOyhQIuP4Om9zqEfZYiVyflROibSsONg==
+ dependencies:
+ prop-types "^15.7.2"
+
console-browserify@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
@@ -5606,7 +5613,7 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
-history@^4.9.0:
+history@4.10.1, history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
@@ -5857,6 +5864,11 @@ immer@7.0.9:
resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.9.tgz#28e7552c21d39dd76feccd2b800b7bc86ee4a62e"
integrity sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A==
+immer@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.0.tgz#08763549ba9dd7d5e2eb4bec504a8315bd9440c2"
+ integrity sha512-jm87NNBAIG4fHwouilCHIecFXp5rMGkiFrAuhVO685UnMAlOneEAnOyzPt8OnP47TC11q/E7vpzZe0WvwepFTg==
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -5994,6 +6006,13 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
+invariant@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ dependencies:
+ loose-envify "^1.0.0"
+
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -7301,7 +7320,7 @@ loglevel@^1.6.8:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0"
integrity sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==
-loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -9719,6 +9738,16 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^1.0.0"
+redux-injectors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/redux-injectors/-/redux-injectors-1.3.0.tgz#7d283633428ccbe22e165903b60f432e63da6947"
+ integrity sha512-ZoyKf8Y0bRqpmJImaVO3jnDKuMXTzMDp3j+b0bqtSuPAgWcHD/2P9gRr4mI1EjgCiheIyQ/JJI8yLG29ijqRaw==
+ dependencies:
+ hoist-non-react-statics "^3.3.2"
+ invariant "^2.2.4"
+ lodash "^4.17.15"
+ redux "^4.0.5"
+
redux-saga@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-1.1.3.tgz#9f3e6aebd3c994bbc0f6901a625f9a42b51d1112"