├── src ├── modules │ ├── Home │ │ ├── home.css │ │ ├── logout.jsx │ │ └── index.jsx │ ├── Register │ │ ├── register.css │ │ ├── actions.js │ │ ├── saga.js │ │ ├── ConfirmAccount │ │ │ ├── saga.js │ │ │ ├── actions.js │ │ │ └── index.jsx │ │ ├── reducer.js │ │ └── index.jsx │ ├── Book │ │ ├── book.css │ │ ├── ManageBook │ │ │ ├── reducer.js │ │ │ ├── saga.js │ │ │ ├── actions.js │ │ │ └── index.jsx │ │ ├── saga.js │ │ ├── reducer.js │ │ ├── actions.js │ │ ├── bookModel.jsx │ │ └── index.jsx │ ├── app │ │ ├── mainSaga.js │ │ ├── mainReducer.js │ │ └── routes.js │ └── Login │ │ ├── login.css │ │ ├── actions.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── index.jsx ├── config │ ├── history.js │ └── store.js ├── helpers │ ├── urls.js │ ├── requests.js │ ├── authRoutes.js │ ├── privateRoutes.js │ └── helpers.js ├── App.test.js ├── components │ ├── Spinner │ │ ├── Spinner.jsx │ │ └── spinner.css │ ├── FlashMessage │ │ └── FlashMessage.jsx │ └── Layouts │ │ └── Private │ │ └── Header.jsx ├── index.css ├── App.css ├── App.js ├── index.js ├── logo.svg └── serviceWorker.js ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── .env.example ├── LICENSE ├── package.json ├── .gitignore ├── CODE_OF_CONDUCT.md └── README.md /src/modules/Home/home.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/Register/register.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spider-yamet/my-boilerplate/master/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spider-yamet/my-boilerplate/master/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spider-yamet/my-boilerplate/master/public/logo512.png -------------------------------------------------------------------------------- /src/config/history.js: -------------------------------------------------------------------------------- 1 | const history = require("history").createBrowserHistory(); 2 | 3 | export default history; -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_APP_DEBUG=true 2 | REACT_APP_NODE_ENV="development" 3 | REACT_APP_DEV_API_URL="http://localhost:3000/api" -------------------------------------------------------------------------------- /src/modules/Book/book.css: -------------------------------------------------------------------------------- 1 | input.error { 2 | border-color: red; 3 | } 4 | 5 | .input-feedback { 6 | color: red; 7 | margin-top: .25rem; 8 | } 9 | 10 | .alert li { 11 | list-style: none; 12 | } -------------------------------------------------------------------------------- /src/helpers/urls.js: -------------------------------------------------------------------------------- 1 | export const urls = { 2 | 'LOGIN_URL': '/auth/login', 3 | 'REGISTER_URL': '/auth/register', 4 | 'BOOK': '/book', 5 | 'VERIFY_CONFIRM_OTP': '/auth/verify-otp', 6 | 'RESEND_CONFIRM_OTP': '/auth/resend-verify-otp' 7 | } -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/modules/app/mainSaga.js: -------------------------------------------------------------------------------- 1 | import { all } from "redux-saga/effects"; 2 | import loginSaga from "../Login/saga"; 3 | import registerSaga from "../Register/saga"; 4 | import bookSaga from "../Book/saga"; 5 | 6 | export function* mainSaga() { 7 | yield all([ 8 | loginSaga(), 9 | registerSaga(), 10 | bookSaga() 11 | ]); 12 | } -------------------------------------------------------------------------------- /src/modules/app/mainReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import loginReducer from "../Login/reducer"; 3 | import registerReducer from "../Register/reducer"; 4 | import bookReducer from "../Book/reducer"; 5 | 6 | export const mainReducer = combineReducers({ 7 | login: loginReducer, 8 | register: registerReducer, 9 | books: bookReducer 10 | }); 11 | -------------------------------------------------------------------------------- /src/modules/Home/logout.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | 4 | class Logout extends Component { 5 | render(){ 6 | localStorage.removeItem('user'); 7 | localStorage.removeItem('token'); 8 | return ( 9 | 10 | ); 11 | } 12 | } 13 | 14 | export default Logout; -------------------------------------------------------------------------------- /src/components/Spinner/Spinner.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './spinner.css'; 3 | 4 | class Spinner extends Component { 5 | render(){ 6 | return ( 7 |
8 |
9 |
10 |
11 | ); 12 | } 13 | } 14 | 15 | export default Spinner; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/Login/login.css: -------------------------------------------------------------------------------- 1 | input.error { 2 | border-color: red; 3 | } 4 | 5 | .input-feedback { 6 | color: red; 7 | margin-top: .25rem; 8 | } 9 | 10 | .alert li { 11 | list-style: none; 12 | } 13 | 14 | button.confirm-button-link { 15 | background: none; 16 | border: 0; 17 | color: blue; 18 | } 19 | 20 | button.confirm-button-link:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | button.resend-btn { 25 | margin-left: 10px; 26 | } -------------------------------------------------------------------------------- /src/helpers/requests.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const API_ROOT = process.env.REACT_APP_NODE_ENV === 'production'? process.env.REACT_APP_PROD_API_URL: process.env.REACT_APP_DEV_API_URL; 4 | 5 | export const request = (method, url, payload=null, headers=1) => { 6 | let requestData = { 7 | method: method, 8 | url: API_ROOT + url 9 | } 10 | if(payload) 11 | requestData.data = payload; 12 | if(headers === 1) 13 | requestData.headers = { 14 | 'Authorization':'Bearer '+ localStorage.getItem('token') 15 | }; 16 | return axios(requestData); 17 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

11 | Edit src/App.js and save to reload. 12 |

13 | 19 | Learn React 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /src/modules/Login/actions.js: -------------------------------------------------------------------------------- 1 | export const LOGIN_REQUESTING = 'LOGIN_REQUESTING'; 2 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; 3 | export const LOGIN_ERROR = 'LOGIN_ERROR'; 4 | export const LOGIN_PAGE_INIT = 'LOGIN_PAGE_INIT'; 5 | 6 | export function loginPageInit() { 7 | return { 8 | type: LOGIN_PAGE_INIT, 9 | }; 10 | } 11 | 12 | export function loginRequest(payload) { 13 | return { 14 | type: LOGIN_REQUESTING, 15 | payload 16 | }; 17 | } 18 | 19 | export function loginError(error) { 20 | return { 21 | type: LOGIN_ERROR, 22 | error, 23 | }; 24 | } 25 | 26 | export function loginSuccess() { 27 | return { 28 | type: LOGIN_SUCCESS, 29 | }; 30 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | //import App from './App'; 5 | import App from './modules/app/routes' 6 | import * as serviceWorker from './serviceWorker'; 7 | import { Provider } from "react-redux"; 8 | import store from "./config/store"; 9 | require('dotenv').config(); 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: https://bit.ly/CRA-PWA 21 | serviceWorker.unregister(); 22 | -------------------------------------------------------------------------------- /src/config/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux"; 2 | import createSagaMiddleware from "redux-saga"; 3 | import { composeWithDevTools } from "redux-devtools-extension/developmentOnly"; 4 | import { mainReducer } from "../modules/app/mainReducer"; 5 | import { mainSaga } from "../modules/app/mainSaga"; 6 | 7 | const composeEnhancers = composeWithDevTools({ 8 | // Specify here name, actionsBlacklist, actionsCreators and other options 9 | }); 10 | 11 | const sagaMiddleware = createSagaMiddleware(); 12 | 13 | const middleware = [sagaMiddleware]; 14 | 15 | const store = createStore( 16 | mainReducer, 17 | composeEnhancers(applyMiddleware(...middleware)) 18 | ); 19 | 20 | sagaMiddleware.run(mainSaga); 21 | 22 | export default store; -------------------------------------------------------------------------------- /src/modules/Register/actions.js: -------------------------------------------------------------------------------- 1 | export const REGISTER_REQUESTING = 'REGISTER_REQUESTING'; 2 | export const REGISTER_SUCCESS = 'REGISTER_SUCCESS'; 3 | export const REGISTER_ERROR = 'REGISTER_ERROR'; 4 | export const REGISTER_PAGE_INIT = 'REGISTER_PAGE_INIT'; 5 | 6 | export function registerPageInit() { 7 | return { 8 | type: REGISTER_PAGE_INIT, 9 | }; 10 | } 11 | 12 | export function registerRequest(payload) { 13 | return { 14 | type: REGISTER_REQUESTING, 15 | payload 16 | }; 17 | } 18 | 19 | export function registerError(error) { 20 | return { 21 | type: REGISTER_ERROR, 22 | error, 23 | }; 24 | } 25 | 26 | export function registerSuccess() { 27 | return { 28 | type: REGISTER_SUCCESS, 29 | }; 30 | } -------------------------------------------------------------------------------- /src/modules/Home/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | class Home extends Component { 5 | render(){ 6 | const user = JSON.parse(localStorage.getItem('user')); 7 | return( 8 |
9 |
10 |
11 |
12 |

Hello, {user.firstName} {user.lastName}!

13 | Lets start collecting the Books? 14 |
15 |
16 |
17 |
18 | ); 19 | } 20 | } 21 | 22 | export default Home; -------------------------------------------------------------------------------- /src/modules/Login/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN_PAGE_INIT, 3 | LOGIN_ERROR, 4 | LOGIN_REQUESTING, 5 | LOGIN_SUCCESS, 6 | } from './actions'; 7 | 8 | // The initial state of the Login Reducer 9 | export const initialState = { 10 | id: '', 11 | password: '', 12 | requesting: false, 13 | successful: false, 14 | messages: [], 15 | errors: {}, 16 | }; 17 | 18 | export default function(state = initialState,actions){ 19 | switch(actions.type){ 20 | case LOGIN_PAGE_INIT: 21 | return {...state, errors:{}}; 22 | case LOGIN_REQUESTING: 23 | return {...state, requesting: true}; 24 | case LOGIN_SUCCESS: 25 | return {...state, successful: true, user:{...actions.payload}}; 26 | case LOGIN_ERROR: 27 | return {...state, successful: false, errors:{...actions.error}}; 28 | default: 29 | return state; 30 | } 31 | } -------------------------------------------------------------------------------- /src/helpers/authRoutes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { checkAuthorization } from '../helpers/helpers'; 5 | 6 | const AuthRoute = ({ 7 | component: Component, 8 | redirect: pathname, 9 | ...rest 10 | }) => { 11 | const Routes = (props) => { 12 | if(checkAuthorization() === false){ 13 | return ( 14 | 17 |
18 | 19 |
20 | } 21 | /> 22 | ); 23 | }else { 24 | return ( 25 | 31 | ); 32 | } 33 | } 34 | return ( 35 | 36 | ); 37 | }; 38 | 39 | AuthRoute.defaultProps = { redirect: '/' }; 40 | 41 | AuthRoute.propTypes = { 42 | component: PropTypes.object.isRequired, 43 | redirect: PropTypes.string, 44 | }; 45 | 46 | export default AuthRoute; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maitray Suthar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-demo-app-saga", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "dotenv": "^8.2.0", 8 | "formik": "^1.5.8", 9 | "react": "^16.11.0", 10 | "react-countdown-now": "^2.1.2", 11 | "react-dom": "^16.11.0", 12 | "react-redux": "^7.1.1", 13 | "react-router": "^5.1.2", 14 | "react-router-dom": "^5.1.2", 15 | "react-scripts": "^3.3.0", 16 | "redux": "^4.0.4", 17 | "redux-saga": "^1.1.1", 18 | "yup": "^0.27.0" 19 | }, 20 | "scripts": { 21 | "start": "PORT=3001 react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "redux-devtools-extension": "^2.13.8" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/Register/saga.js: -------------------------------------------------------------------------------- 1 | import { put, all, call, takeLatest } from "redux-saga/effects"; 2 | import { request } from '../../helpers/requests'; 3 | import { urls } from '../../helpers/urls'; 4 | import { 5 | REGISTER_REQUESTING, 6 | registerSuccess, 7 | registerError, 8 | } from "./actions"; 9 | import { redirectForConfirm } from './ConfirmAccount/actions'; 10 | import { browserRedirect } from '../../helpers/helpers'; 11 | import otpSaga from './ConfirmAccount/saga'; 12 | 13 | //Register API call 14 | function registerCall(payload) { 15 | return request('post', urls.REGISTER_URL, payload); 16 | } 17 | 18 | // Register Worker 19 | function* registerWorker({ payload }) { 20 | try { 21 | let response = yield call(registerCall, payload); 22 | yield put(registerSuccess()); 23 | yield put(redirectForConfirm(response.data.data.email)); 24 | yield call(browserRedirect, '/confirm-account'); 25 | } catch (err) { 26 | yield put(registerError(err.response.data)); 27 | } 28 | } 29 | 30 | // Register Watcher 31 | export default function* registerSaga() { 32 | yield all([ 33 | takeLatest(REGISTER_REQUESTING, registerWorker), 34 | otpSaga() 35 | ]); 36 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /src/helpers/privateRoutes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { checkAuthorization } from '../helpers/helpers'; 5 | import Header from '../components/Layouts/Private/Header'; 6 | 7 | const PrivateRoute = ({ 8 | component: Component, 9 | redirect: pathname, 10 | ...rest 11 | }) => { 12 | const Routes = (props) => { 13 | if(checkAuthorization() === true){ 14 | return ( 15 | 18 |
19 |
20 | 21 |
22 | } 23 | /> 24 | ); 25 | }else { 26 | return ( 27 | 33 | ); 34 | } 35 | } 36 | return ( 37 | 38 | ); 39 | }; 40 | 41 | PrivateRoute.defaultProps = { redirect: '/login' }; 42 | 43 | PrivateRoute.propTypes = { 44 | component: PropTypes.object.isRequired, 45 | redirect: PropTypes.string, 46 | }; 47 | 48 | export default PrivateRoute; -------------------------------------------------------------------------------- /src/modules/Login/saga.js: -------------------------------------------------------------------------------- 1 | import { put, all, call, takeLatest } from "redux-saga/effects"; 2 | import { request } from '../../helpers/requests'; 3 | import { browserRedirect } from '../../helpers/helpers'; 4 | import { urls } from '../../helpers/urls'; 5 | import { 6 | LOGIN_REQUESTING, 7 | loginSuccess, 8 | loginError, 9 | } from "./actions"; 10 | 11 | //Login API call 12 | function loginCall(payload) { 13 | return request('post', urls.LOGIN_URL, payload); 14 | } 15 | 16 | // LOGIN Worker 17 | function* loginWorker({ payload }) { 18 | try { 19 | let response = yield call(loginCall, payload); 20 | response = response.data; 21 | localStorage.removeItem('user'); 22 | localStorage.setItem('token', response.data.token); 23 | localStorage.setItem( 24 | 'user', 25 | JSON.stringify({ 26 | id: response.data._id, 27 | firstName: response.data.firstName, 28 | lastName: response.data.lastName 29 | }), 30 | ); 31 | yield put(loginSuccess()); 32 | yield call(browserRedirect, '/'); 33 | } catch (err) { 34 | yield put(loginError(err.response.data)); 35 | } 36 | } 37 | 38 | // Login Watcher 39 | export default function* loginSaga() { 40 | yield all([ 41 | takeLatest(LOGIN_REQUESTING, loginWorker), 42 | ]); 43 | } -------------------------------------------------------------------------------- /src/components/FlashMessage/FlashMessage.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class FlashMessage extends Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | const { data } = this.props; 10 | const { alertClass } = this.props; 11 | return ( 12 |
13 | {data.constructor === String &&
  • {data}
  • } 14 | {data.constructor === Object && 15 | Object.keys(data).map(key => { 16 | return ( 17 |
  • 18 | {key} {data[key]} 19 |
  • 20 | ); 21 | })} 22 | {data.constructor === Array && 23 | data.map((item, index) => { 24 | return
  • {item.msg}
  • ; 25 | })} 26 | 29 |
    30 | ) 31 | } 32 | } 33 | 34 | export default FlashMessage; 35 | -------------------------------------------------------------------------------- /src/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import history from '../config/history'; 3 | 4 | export const DisplayFormikState = props => 5 |
    6 |

     

    7 |
    14 |       props ={' '}
    15 |       {JSON.stringify(props, null, 2)}
    16 |     
    17 |
    ; 18 | 19 | export const browserRedirect = location => { 20 | history.push(location); 21 | } 22 | 23 | export const parseJwt = token => { 24 | if (token) { 25 | const base64Url = token.split('.')[1]; 26 | const base64 = base64Url.replace('-', '+').replace('_', '/'); 27 | return JSON.parse(window.atob(base64)); 28 | } 29 | return null; 30 | }; 31 | 32 | export const checkAuthorization = () => { 33 | const storedToken = localStorage.getItem('token'); 34 | 35 | if (storedToken) { 36 | const tokenPayload = parseJwt(storedToken); 37 | 38 | const expiration = new Date(tokenPayload.exp * 1000).getTime(); 39 | const current = new Date().getTime(); 40 | 41 | if (current > expiration) return false; 42 | 43 | return true; 44 | } 45 | 46 | return false; 47 | }; -------------------------------------------------------------------------------- /src/modules/Book/ManageBook/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | BOOK_ADD_PAGE_INIT, 3 | BOOK_ADD_ERROR, 4 | BOOK_ADD_REQUESTING, 5 | BOOK_ADD_SUCCESS, 6 | BOOK_UPDATE_ERROR, 7 | BOOK_UPDATE_REQUESTING, 8 | BOOK_UPDATE_SUCCESS, 9 | } from './actions'; 10 | 11 | // The initial state of the Login Reducer 12 | export const initialState = { 13 | requesting: false, 14 | successful: false, 15 | messages: [], 16 | errors: {}, 17 | addedBook: {}, 18 | updatedBook: {} 19 | }; 20 | 21 | export default function(state = initialState,actions){ 22 | switch(actions.type){ 23 | case BOOK_ADD_PAGE_INIT: 24 | return {...state, errors:{}}; 25 | case BOOK_ADD_REQUESTING: 26 | return {...state, requesting: true, errors:{}}; 27 | case BOOK_ADD_SUCCESS: 28 | return {...state, successful: true, errors:{}, addedBook:{...actions.payload}}; 29 | case BOOK_ADD_ERROR: 30 | return {...state, successful: false, errors:{...actions.error}}; 31 | case BOOK_UPDATE_REQUESTING: 32 | return {...state, requesting: true, errors:{}}; 33 | case BOOK_UPDATE_SUCCESS: 34 | return {...state, successful: true, errors:{}, updatedBook:{...actions.payload}}; 35 | case BOOK_UPDATE_ERROR: 36 | return {...state, successful: false, errors:{...actions.error}}; 37 | default: 38 | return state; 39 | } 40 | } -------------------------------------------------------------------------------- /src/modules/Book/ManageBook/saga.js: -------------------------------------------------------------------------------- 1 | import { put, all, call, takeLatest } from "redux-saga/effects"; 2 | import { request } from '../../../helpers/requests'; 3 | import { browserRedirect } from '../../../helpers/helpers'; 4 | import { urls } from '../../../helpers/urls'; 5 | import { 6 | BOOK_ADD_REQUESTING, 7 | bookAddSuccess, 8 | bookAddError, 9 | BOOK_UPDATE_REQUESTING, 10 | bookUpdateSuccess, 11 | bookUpdateError, 12 | } from "./actions"; 13 | 14 | //bookAdd API call 15 | function bookAddCall(payload) { 16 | return request('post', urls.BOOK, payload); 17 | } 18 | 19 | //bookUpdate API call 20 | function bookUpdateCall(payload,id) { 21 | return request('put', urls.BOOK+"/"+id, payload); 22 | } 23 | 24 | // bookAdd Worker 25 | function* bookAddWorker({ payload }) { 26 | try { 27 | yield call(bookAddCall, payload); 28 | yield put(bookAddSuccess()); 29 | yield call(browserRedirect, '/book'); 30 | } catch (err) { 31 | yield put(bookAddError(err.response.data)); 32 | } 33 | } 34 | 35 | //bookUpdate Worker 36 | function* bookUpdateWorker({payload, id}) { 37 | try { 38 | yield call(bookUpdateCall, payload, id); 39 | yield put(bookUpdateSuccess()); 40 | yield call(browserRedirect, '/book'); 41 | } catch (err) { 42 | yield put(bookUpdateError(err.response.data)); 43 | } 44 | } 45 | 46 | // bookAdd Watcher 47 | export default function* bookAddSaga() { 48 | yield all([ 49 | takeLatest(BOOK_ADD_REQUESTING, bookAddWorker), 50 | takeLatest(BOOK_UPDATE_REQUESTING, bookUpdateWorker) 51 | ]); 52 | } -------------------------------------------------------------------------------- /src/modules/Register/ConfirmAccount/saga.js: -------------------------------------------------------------------------------- 1 | import { put, all, call, takeLatest } from "redux-saga/effects"; 2 | import { request } from '../../../helpers/requests'; 3 | import { urls } from '../../../helpers/urls'; 4 | import { 5 | VERIFY_OTP_REQUESTING, 6 | verifyOTPSuccess, 7 | verifyOTPError, 8 | RESEND_OTP_REQUESTING, 9 | resendOTPSuccess, 10 | resendOTPError 11 | } from "./actions"; 12 | import { browserRedirect } from '../../../helpers/helpers'; 13 | 14 | //Verify OTP API call 15 | function verifyOTPCall(payload) { 16 | return request('post', urls.VERIFY_CONFIRM_OTP, payload); 17 | } 18 | 19 | //Resend OTP API call 20 | function resendOTPCall(payload) { 21 | return request('post', urls.RESEND_CONFIRM_OTP, payload); 22 | } 23 | 24 | // Verify OTP Worker 25 | function* verifyOTPWorker({ payload }) { 26 | try { 27 | let response = yield call(verifyOTPCall, payload); 28 | yield put(verifyOTPSuccess(response)); 29 | yield call(browserRedirect, '/login'); 30 | } catch (err) { 31 | yield put(verifyOTPError(err.response.data)); 32 | } 33 | } 34 | 35 | // Resend OTP Worker 36 | function* resendOTPWorker(email) { 37 | try { 38 | let response = yield call(resendOTPCall, email); 39 | yield put(resendOTPSuccess(response)); 40 | } catch (err) { 41 | yield put(resendOTPError(err.response.data)); 42 | } 43 | } 44 | 45 | // Verify OTP Watcher 46 | export default function* verifyOTPSaga() { 47 | yield all([ 48 | takeLatest(VERIFY_OTP_REQUESTING, verifyOTPWorker), 49 | takeLatest(RESEND_OTP_REQUESTING, resendOTPWorker) 50 | ]); 51 | } -------------------------------------------------------------------------------- /src/modules/Book/ManageBook/actions.js: -------------------------------------------------------------------------------- 1 | export const BOOK_ADD_REQUESTING = 'BOOK_ADD_REQUESTING'; 2 | export const BOOK_ADD_SUCCESS = 'BOOK_ADD_SUCCESS'; 3 | export const BOOK_ADD_ERROR = 'BOOK_ADD_ERROR'; 4 | export const BOOK_ADD_PAGE_INIT = 'BOOK_ADD_PAGE_INIT'; 5 | export const BOOK_UPDATE_REQUESTING = 'BOOK_UPDATE_REQUESTING'; 6 | export const BOOK_UPDATE_SUCCESS = 'BOOK_UPDATE_SUCCESS'; 7 | export const BOOK_UPDATE_ERROR = 'BOOK_UPDATE_ERROR'; 8 | export const BOOK_UPDATE_PAGE_INIT = 'BOOK_UPDATE_PAGE_INIT'; 9 | 10 | export function bookAddPageInit() { 11 | return { 12 | type: BOOK_ADD_PAGE_INIT, 13 | }; 14 | } 15 | 16 | export function bookAddRequest(payload) { 17 | return { 18 | type: BOOK_ADD_REQUESTING, 19 | payload 20 | }; 21 | } 22 | 23 | export function bookAddError(error) { 24 | return { 25 | type: BOOK_ADD_ERROR, 26 | error, 27 | }; 28 | } 29 | 30 | export function bookAddSuccess() { 31 | return { 32 | type: BOOK_ADD_SUCCESS, 33 | }; 34 | } 35 | 36 | export function bookUpdatePageInit(id) { 37 | return { 38 | type: BOOK_UPDATE_PAGE_INIT, 39 | id 40 | }; 41 | } 42 | 43 | export function bookUpdateRequest(payload,id) { 44 | return { 45 | type: BOOK_UPDATE_REQUESTING, 46 | payload, 47 | id 48 | }; 49 | } 50 | 51 | export function bookUpdateError(error) { 52 | return { 53 | type: BOOK_UPDATE_ERROR, 54 | error, 55 | }; 56 | } 57 | 58 | export function bookUpdateSuccess() { 59 | return { 60 | type: BOOK_UPDATE_SUCCESS, 61 | }; 62 | } -------------------------------------------------------------------------------- /src/modules/app/routes.js: -------------------------------------------------------------------------------- 1 | import { Switch } from 'react-router-dom'; 2 | import React, { Suspense, lazy } from 'react'; 3 | import { Router } from 'react-router'; 4 | import history from '../../config/history'; 5 | import PrivateRoute from '../../helpers/privateRoutes'; // Private Routes, Will only accessible after Login 6 | import AuthRoute from '../../helpers/authRoutes'; // Auth Routes, Will only accessible before login. 7 | 8 | // Lazy loading of all the components. 9 | const Home = lazy(() => import('../Home')); 10 | const Logout = lazy(() => import('../Home/logout')); 11 | const Login = lazy(() => import('../Login')); 12 | const Register = lazy(() => import('../Register')); 13 | const ConfirmAccount = lazy(() => import('../Register/ConfirmAccount')); 14 | const Book = lazy(() => import('../Book')); 15 | const ManageBook = lazy(() => import('../Book/ManageBook')) 16 | 17 | // Root routes 18 | const Routes = () => ( 19 | 20 | Loading...}> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | 35 | export default Routes; -------------------------------------------------------------------------------- /src/modules/Register/ConfirmAccount/actions.js: -------------------------------------------------------------------------------- 1 | export const VERIFY_OTP_REQUESTING = 'VERIFY_OTP_REQUESTING'; 2 | export const VERIFY_OTP_SUCCESS = 'VERIFY_OTP_SUCCESS'; 3 | export const VERIFY_OTP_ERROR = 'VERIFY_OTP_ERROR'; 4 | export const OTP_PAGE_INIT = 'OTP_PAGE_INIT'; 5 | export const REDIRECT_FOR_CONFIRM = 'REDIRECT_FOR_CONFIRM'; 6 | export const CLEAR_CONFIRM_DATA = 'CLEAR_CONFIRM_DATA'; 7 | export const RESEND_OTP_REQUESTING = 'RESEND_OTP_REQUESTING'; 8 | export const RESEND_OTP_SUCCESS = 'RESEND_OTP_SUCCESS'; 9 | export const RESEND_OTP_ERROR = 'RESEND_OTP_ERROR'; 10 | 11 | 12 | export function otpPageInit() { 13 | return { 14 | type: OTP_PAGE_INIT, 15 | }; 16 | } 17 | 18 | export function verifyOTPRequest(payload) { 19 | return { 20 | type: VERIFY_OTP_REQUESTING, 21 | payload 22 | }; 23 | } 24 | 25 | export function verifyOTPError(error) { 26 | return { 27 | type: VERIFY_OTP_ERROR, 28 | error, 29 | }; 30 | } 31 | 32 | export function verifyOTPSuccess() { 33 | return { 34 | type: VERIFY_OTP_SUCCESS, 35 | }; 36 | } 37 | 38 | export function redirectForConfirm(email) { 39 | return { 40 | type: REDIRECT_FOR_CONFIRM, 41 | email 42 | }; 43 | } 44 | 45 | export function clearConfirmData() { 46 | return { 47 | type: CLEAR_CONFIRM_DATA, 48 | }; 49 | } 50 | 51 | export function resendOTPRequest(email) { 52 | return { 53 | type: RESEND_OTP_REQUESTING, 54 | email 55 | }; 56 | } 57 | 58 | export function resendOTPError(error) { 59 | return { 60 | type: RESEND_OTP_ERROR, 61 | error, 62 | }; 63 | } 64 | 65 | export function resendOTPSuccess(payload) { 66 | return { 67 | type: RESEND_OTP_SUCCESS, 68 | payload 69 | }; 70 | } -------------------------------------------------------------------------------- /src/components/Spinner/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner2 { 2 | position: absolute; 3 | left: 50%; 4 | top: 50%; 5 | height:60px; 6 | width:60px; 7 | margin:0px auto; 8 | -webkit-animation: rotation .6s infinite linear; 9 | -moz-animation: rotation .6s infinite linear; 10 | -o-animation: rotation .6s infinite linear; 11 | animation: rotation .6s infinite linear; 12 | border-left:6px solid rgba(0,174,239,.15); 13 | border-right:6px solid rgba(0,174,239,.15); 14 | border-bottom:6px solid rgba(0,174,239,.15); 15 | border-top:6px solid rgba(0,174,239,.8); 16 | border-radius:100%; 17 | } 18 | 19 | @-webkit-keyframes rotation { 20 | from {-webkit-transform: rotate(0deg);} 21 | to {-webkit-transform: rotate(359deg);} 22 | } 23 | @-moz-keyframes rotation { 24 | from {-moz-transform: rotate(0deg);} 25 | to {-moz-transform: rotate(359deg);} 26 | } 27 | @-o-keyframes rotation { 28 | from {-o-transform: rotate(0deg);} 29 | to {-o-transform: rotate(359deg);} 30 | } 31 | @keyframes rotation { 32 | from {transform: rotate(0deg);} 33 | to {transform: rotate(359deg);} 34 | } 35 | 36 | .loader-wrapper { 37 | position: fixed; 38 | top: 0; 39 | left: 0; 40 | width: 100%; 41 | height: 100%; 42 | z-index: 1006; 43 | background: rgba(255, 255, 255, 0.7); 44 | } 45 | 46 | .loader-wrapper .spinner { 47 | display: block; 48 | position: relative; 49 | left: 50%; 50 | top: 50%; 51 | width: 75px; 52 | height: 75px; 53 | margin: -75px 0 0 -75px; 54 | -webkit-animation: rotation .6s infinite linear; 55 | -moz-animation: rotation .6s infinite linear; 56 | -o-animation: rotation .6s infinite linear; 57 | animation: rotation .6s infinite linear; 58 | border-left:6px solid rgba(0,174,239,.15); 59 | border-right:6px solid rgba(0,174,239,.15); 60 | border-bottom:6px solid rgba(0,174,239,.15); 61 | border-top:6px solid rgba(0,174,239,.8); 62 | border-radius:100%; 63 | } -------------------------------------------------------------------------------- /src/modules/Book/saga.js: -------------------------------------------------------------------------------- 1 | import { put, all, call, takeLatest } from "redux-saga/effects"; 2 | import { 3 | BOOK_PAGE_INIT, 4 | BOOK_DETAIL_INIT, 5 | BOOK_DELETE_INIT, 6 | bookError, 7 | bookSuccess, 8 | bookDetailError, 9 | bookDetailSuccess, 10 | bookDeleteError, 11 | bookDeleteSuccess, 12 | bookPageInit 13 | } from "./actions"; 14 | import { request } from '../../helpers/requests'; 15 | import { urls } from '../../helpers/urls'; 16 | import manageBookSaga from './ManageBook/saga'; 17 | 18 | //Book API calls 19 | function bookCall() { 20 | return request('get', urls.BOOK); 21 | } 22 | 23 | function bookDetailCall(id) { 24 | return request('get', urls.BOOK+'/'+id); 25 | } 26 | 27 | function bookDeleteCall(id) { 28 | return request('delete', urls.BOOK+'/'+id); 29 | } 30 | 31 | // Book Workers 32 | function* bookWorker() { 33 | try { 34 | let response = yield call(bookCall); 35 | response = response.data.data; 36 | yield put(bookSuccess(response)); 37 | } catch (err) { 38 | yield put(bookError(err.response.data.data)); 39 | } 40 | } 41 | 42 | function* bookDetailWorker(payload) { 43 | try { 44 | let response = yield call(bookDetailCall, payload.id); 45 | response = response.data.data; 46 | yield put(bookDetailSuccess(response)); 47 | } catch (err) { 48 | yield put(bookDetailError(err.response.data.data)); 49 | } 50 | } 51 | 52 | function* bookDeleteWorker(payload) { 53 | try { 54 | let response = yield call(bookDeleteCall, payload.id); 55 | response = response.data; 56 | yield put(bookDeleteSuccess(response)); 57 | yield put(bookPageInit()); 58 | } catch (err) { 59 | yield put(bookDeleteError(err.response.data)); 60 | } 61 | } 62 | 63 | // Book Watcher 64 | export default function* bookSaga() { 65 | yield all([ 66 | takeLatest(BOOK_PAGE_INIT, bookWorker), 67 | takeLatest(BOOK_DETAIL_INIT, bookDetailWorker), 68 | takeLatest(BOOK_DELETE_INIT, bookDeleteWorker), 69 | manageBookSaga() 70 | ]); 71 | } -------------------------------------------------------------------------------- /src/components/Layouts/Private/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | class Header extends Component { 5 | render() { 6 | const user = JSON.parse(localStorage.getItem('user')); 7 | return ( 8 | 33 | ); 34 | } 35 | } 36 | 37 | export default Header; -------------------------------------------------------------------------------- /src/modules/Book/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | BOOK_PAGE_INIT, 3 | BOOK_ERROR, 4 | BOOK_SUCCESS, 5 | BOOK_DETAIL_INIT, 6 | BOOK_DETAIL_ERROR, 7 | BOOK_DETAIL_SUCCESS, 8 | BOOK_DETAIL_CLOSE, 9 | BOOK_DELETE_ERROR, 10 | BOOK_DELETE_SUCCESS, 11 | RELEASE_STATE_DATA 12 | } from './actions'; 13 | import { combineReducers } from "redux"; 14 | import manageBookReducer from './ManageBook/reducer'; 15 | 16 | // The initial state of the Login Reducer 17 | export const initialState = { 18 | successful: false, 19 | messages: [], 20 | errors: [], 21 | books: [], 22 | selectedBook: {}, 23 | selectedBookError: {}, 24 | deleteBook: {} 25 | }; 26 | 27 | const bookReducers = function(state = initialState,actions){ 28 | switch(actions.type){ 29 | case BOOK_PAGE_INIT: 30 | return {...state, errors:[], books: []}; 31 | case BOOK_SUCCESS: 32 | return {...state, successful: true, books:[...actions.payload]}; 33 | case BOOK_ERROR: 34 | return {...state, successful: false, errors:[...actions.error]}; 35 | case BOOK_DETAIL_INIT: 36 | return {...state, selectedBookError:{}, selectedBook: {}}; 37 | case BOOK_DETAIL_SUCCESS: 38 | return {...state, selectedBook: {...actions.payload}}; 39 | case BOOK_DETAIL_ERROR: 40 | return {...state, selectedBookError:{...actions.error}}; 41 | case BOOK_DETAIL_CLOSE: 42 | return {...state, selectedBookError:{}, selectedBook: {}}; 43 | case BOOK_DELETE_SUCCESS: 44 | return {...state, deleteBook: {...actions.payload}}; 45 | case BOOK_DELETE_ERROR: 46 | return {...state, selectedBookError:{...actions.error}}; 47 | case RELEASE_STATE_DATA: 48 | return {...state, errors:[], books: [], selectedBook: {},selectedBookError: {},deleteBook: {}} 49 | default: 50 | return state; 51 | } 52 | } 53 | 54 | export default combineReducers({ 55 | list_book : bookReducers, 56 | manage_book: manageBookReducer 57 | }); -------------------------------------------------------------------------------- /src/modules/Book/actions.js: -------------------------------------------------------------------------------- 1 | export const BOOK_PAGE_INIT = 'BOOK_PAGE_INIT'; 2 | export const BOOK_ERROR = 'BOOK_ERROR'; 3 | export const BOOK_SUCCESS = 'BOOK_SUCCESS'; 4 | export const BOOK_DETAIL_INIT = 'BOOK_DETAIL_INIT'; 5 | export const BOOK_DETAIL_ERROR = 'BOOK_DETAIL_ERROR'; 6 | export const BOOK_DETAIL_SUCCESS = 'BOOK_DETAIL_SUCCESS'; 7 | export const BOOK_DETAIL_CLOSE = 'BOOK_DETAIL_CLOSE'; 8 | export const BOOK_DELETE_INIT = 'BOOK_DELETE_INIT'; 9 | export const BOOK_DELETE_ERROR = 'BOOK_DELETE_ERROR'; 10 | export const BOOK_DELETE_SUCCESS = 'BOOK_DELETE_SUCCESS'; 11 | export const RELEASE_STATE_DATA = 'RELEASE_STATE_DATA'; 12 | 13 | export function bookPageInit() { 14 | return { 15 | type: BOOK_PAGE_INIT, 16 | }; 17 | } 18 | 19 | export function bookError(error) { 20 | return { 21 | type: BOOK_ERROR, 22 | error, 23 | }; 24 | } 25 | 26 | export function bookSuccess(payload) { 27 | return { 28 | type: BOOK_SUCCESS, 29 | payload 30 | }; 31 | } 32 | 33 | export function bookDetailInit(id) { 34 | return { 35 | type: BOOK_DETAIL_INIT, 36 | id 37 | }; 38 | } 39 | 40 | export function bookDetailError(error) { 41 | return { 42 | type: BOOK_DETAIL_ERROR, 43 | error, 44 | }; 45 | } 46 | 47 | export function bookDetailSuccess(payload) { 48 | return { 49 | type: BOOK_DETAIL_SUCCESS, 50 | payload 51 | }; 52 | } 53 | 54 | export function bookDetailClose() { 55 | return { 56 | type: BOOK_DETAIL_CLOSE, 57 | }; 58 | } 59 | 60 | export function bookDeleteInit(id) { 61 | return { 62 | type: BOOK_DELETE_INIT, 63 | id 64 | }; 65 | } 66 | 67 | export function bookDeleteError(error) { 68 | return { 69 | type: BOOK_DELETE_ERROR, 70 | error, 71 | }; 72 | } 73 | 74 | export function bookDeleteSuccess(payload) { 75 | return { 76 | type: BOOK_DELETE_SUCCESS, 77 | payload 78 | }; 79 | } 80 | 81 | export function releaseStateData(){ 82 | return { 83 | type: RELEASE_STATE_DATA, 84 | } 85 | } -------------------------------------------------------------------------------- /src/modules/Register/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER_PAGE_INIT, 3 | REGISTER_ERROR, 4 | REGISTER_REQUESTING, 5 | REGISTER_SUCCESS, 6 | } from './actions'; 7 | import { 8 | OTP_PAGE_INIT, 9 | VERIFY_OTP_ERROR, 10 | VERIFY_OTP_REQUESTING, 11 | VERIFY_OTP_SUCCESS, 12 | REDIRECT_FOR_CONFIRM, 13 | CLEAR_CONFIRM_DATA, 14 | RESEND_OTP_ERROR, 15 | RESEND_OTP_REQUESTING, 16 | RESEND_OTP_SUCCESS, 17 | } from './ConfirmAccount/actions'; 18 | 19 | // The initial state of the Login Reducer 20 | export const initialState = { 21 | id: '', 22 | password: '', 23 | requesting: false, 24 | successful: false, 25 | messages: [], 26 | errors: {}, 27 | user: {}, 28 | otp_errors: {}, 29 | confirm_email: '', 30 | resend_success: {} 31 | }; 32 | 33 | export default function(state = initialState,actions){ 34 | switch(actions.type){ 35 | case REGISTER_PAGE_INIT: 36 | return {...state, errors:{}}; 37 | case REGISTER_REQUESTING: 38 | return {...state, requesting: true}; 39 | case REGISTER_SUCCESS: 40 | return {...state, requesting: false, successful: true, errors:{}}; 41 | case REGISTER_ERROR: 42 | return {...state, requesting: false, successful: false, errors:{...actions.error}}; 43 | case OTP_PAGE_INIT: 44 | return {...state, otp_errors:{}, resend_success: {}}; 45 | case VERIFY_OTP_REQUESTING: 46 | return {...state, resend_success:{}, requesting: true}; 47 | case VERIFY_OTP_SUCCESS: 48 | return {...state, requesting: false, successful: true, otp_errors:{}}; 49 | case VERIFY_OTP_ERROR: 50 | return {...state, requesting: false, successful: false, otp_errors:{...actions.error}}; 51 | case REDIRECT_FOR_CONFIRM: 52 | return {...state, confirm_email: actions.email}; 53 | case CLEAR_CONFIRM_DATA: 54 | return {...state, confirm_email: '', otp_errors:{}}; 55 | case RESEND_OTP_REQUESTING: 56 | return {...state, requesting: true}; 57 | case RESEND_OTP_SUCCESS: 58 | return {...state, requesting: false, successful: true, otp_errors:{}, resend_success:{...actions.payload.data}}; 59 | case RESEND_OTP_ERROR: 60 | return {...state, requesting: false, successful: false, otp_errors:{...actions.error}}; 61 | default: 62 | return state; 63 | } 64 | } -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 29 | React App 30 | 31 | 32 | 33 |
    34 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/modules/Book/bookModel.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class BookModel extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.handleCloseClick = this.handleCloseClick.bind(this); 7 | this.handleDeleteClick = this.handleDeleteClick.bind(this); 8 | } 9 | componentDidMount() { 10 | const { handleModalCloseClick } = this.props; 11 | const $ = window.$; 12 | $(this.modal).on('hidden.bs.modal', handleModalCloseClick); 13 | } 14 | handleCloseClick() { 15 | const $ = window.$; 16 | const { handleModalCloseClick } = this.props; 17 | $(this.modal).modal('hide'); 18 | handleModalCloseClick(); 19 | } 20 | 21 | handleDeleteClick(id) { 22 | const $ = window.$; 23 | $(this.modal).modal('hide'); 24 | const { handleDeleteClick } = this.props; 25 | handleDeleteClick(id); 26 | } 27 | 28 | showModelBody() { 29 | const { isForDelete } = this.props; 30 | if(!isForDelete) { 31 | return ( 32 |
    33 |

    Book Title: {this.props.selectedBook.title}

    34 |

    Book ISBN: {this.props.selectedBook.isbn}

    35 |

    Book Description: {this.props.selectedBook.description}

    36 |

    Stored time: {this.props.selectedBook.createdAt}

    37 |
    38 | ); 39 | }else{ 40 | return ( 41 |
    42 |

    Are you sure to Delete Book: {this.props.selectedBook.title} ?

    43 |
    44 | ); 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 |
    this.modal = modal} id="exampleModal" tabIndex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> 51 |
    52 |
    53 |
    54 |
    Book : {this.props.selectedBook.title}
    55 | 58 |
    59 |
    60 | {this.showModelBody()} 61 |
    62 |
    63 | {this.props.isForDelete && 64 | 65 | } 66 | 67 |
    68 |
    69 |
    70 |
    71 | ); 72 | } 73 | } 74 | 75 | export default BookModel; -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at maitraysuthar@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # React JS with Redux and Saga Project Structure 3 | [![Author](http://img.shields.io/badge/author-@maitraysuthar-blue.svg)](https://www.linkedin.com/in/maitray-suthar/) [![GitHub license](https://img.shields.io/github/license/maitraysuthar/rest-api-nodejs-mongodb.svg)](https://github.com/maitraysuthar/react-redux-saga-boilerplate/blob/master/LICENSE) ![GitHub repo size](https://img.shields.io/github/repo-size/maitraysuthar/react-redux-saga-boilerplate) 4 | 5 | A ready-to-use boilerplate for React JS with Redux and Saga. 6 | 7 | ## Project Overview 8 | 9 | This is a basic project structure with repeatative use cases. Added some essential feature for every projects. It is very useful to build mid to complex level project. This project structure is based on **NodeJs api boilerplate app:** 10 | 11 | I had tried to maintain the code structure easy as any beginner can also adopt the flow and start building a great app. Project is open for suggestions, Bug reports and pull requests. 12 | 13 | ## Is this project deserves a small treat? 14 | 15 | If you consider my project as helpful stuff, You can appreciate me or my hard work and time spent to create this helpful structure with buying a coffee for me. 16 | 17 | 18 | ## Features 19 | 20 | |Feature|Details | 21 | |--|--| 22 | | Structure| Project is build with extenensible and flexible **Moduler** pattern| 23 | | Authentication| Basic Authentication (Register/Login)| 24 | | Confirm Account| Account confirmation with OTP verification| 25 | | Route Protection| Route protection with middleware and localstorage| 26 | | Lazy Loading| Added **Lazy Loading** of components to fasten the execution process of application| 27 | | App State Management| Application level state management with **Redux**| 28 | | Async Call| Managed async calls with **Saga** middleware| 29 | | Forms| Managed apllication forms & validations with **Formik** and **Yup**| 30 | 31 | ## Software Requirements 32 | 33 | - Node.js **8+** 34 | 35 | ## How to install 36 | 37 | ### Using Git (recommended) 38 | 39 | 1. Clone the project from github. Change "myproject" to your project name. 40 | 41 | ```bash 42 | git clone https://github.com/maitraysuthar/react-redux-saga-boilerplate.git ./myproject 43 | ``` 44 | 45 | ### Using manual download ZIP 46 | 47 | 1. Download repository 48 | 2. Uncompress to your desired directory 49 | 50 | ### Install npm dependencies after installing (Git or manual download) 51 | 52 | ```bash 53 | cd myproject 54 | npm install 55 | ``` 56 | 57 | ### Setting up environments 58 | 59 | 1. You will find a file named `.env.example` on root directory of project. 60 | 2. Create a new file by copying and pasting the file and then renaming it to just `.env` 61 | ```bash 62 | cp .env.example .env 63 | ``` 64 | 3. The file `.env` is already ignored, so you never commit your credentials. 65 | 4. Change the values of the file to your environment. Helpful comments added to `.env.example` file to understand the constants. 66 | 67 | ## How to run 68 | 69 | ```bash 70 | npm start 71 | ``` 72 | 73 | ## New Module 74 | 75 | All the modules of the project will be in `/src/modules/` folder, If you need to add more modules to the project just create a new folder in the same folder. 76 | 77 | ##### Every folder contains following files: 78 | - Component file (`index.jsx`) 79 | - Actions file (`actions.js`) 80 | - Reducer file (`reducer.js`) 81 | - Saga file (`saga.js`) 82 | - Style file (`[module].css`) 83 | - Sub module folder, if any. 84 | 85 | ##### Root module: 86 | Module's root module folder is `/src/modules/app/` it contains main **Routes file (`routes.js`)**, **Reducer file (`mainReducer.js`)** and **Saga file (`mainSaga.js`)**. You will need to add your every component,reducer & saga to make your module work. 87 | 88 | ## Found any bug? Need any feature? 89 | 90 | Every project needs improvements, Feel free to report any bugs or improvements. Pull requests are always welcome. 91 | 92 | ## License 93 | 94 | This project is open-sourced software licensed under the MIT License. See the LICENSE file for more information. -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/modules/Book/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link, withRouter } from 'react-router-dom'; 4 | import './book.css'; 5 | import PropTypes from 'prop-types'; 6 | import { bookPageInit,bookDetailInit,bookDetailClose,bookDeleteInit,releaseStateData } from './actions'; 7 | import FlashMessage from '../../components/FlashMessage/FlashMessage'; 8 | import ViewBookModel from './bookModel'; 9 | 10 | class Book extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.handleModalShowClick = this.handleModalShowClick.bind(this); 14 | this.handleModalCloseClick = this.handleModalCloseClick.bind(this); 15 | this.handleDeleteClick = this.handleDeleteClick.bind(this); 16 | this.state = { 17 | isForDelete:false 18 | } 19 | } 20 | 21 | handleModalShowClick (isForDelete) { 22 | return event => { 23 | this.props.getBookDetail(event.currentTarget.dataset.id); 24 | if(isForDelete){ 25 | this.setState({ 26 | isForDelete: true 27 | }) 28 | } 29 | } 30 | } 31 | 32 | handleDeleteClick (id) { 33 | this.props.deleteBookRequest(id); 34 | } 35 | 36 | handleModalCloseClick() { 37 | this.props.detailModalClose(); 38 | this.setState({ 39 | isForDelete: false 40 | }) 41 | } 42 | 43 | componentDidMount(){ 44 | this.props.getBooks(); 45 | } 46 | 47 | componentWillUnmount() { 48 | this.props.releaseData(); 49 | } 50 | 51 | renderBooks(handleModalShow){ 52 | if(this.props.books.length === 0){ 53 | return No books!; 54 | }else{ 55 | return this.props.books.map(function(data, id) { 56 | return ( 57 | 58 | {id + 1} 59 | {data.title} 60 | {data.isbn} 61 | 62 | 67 | Update 68 | 73 | 74 | 75 | ); 76 | }); 77 | } 78 | } 79 | 80 | render(){ 81 | return( 82 |
    83 |
    84 |
    85 | {Object.keys(this.props.errors).length > 0 && 86 |
    87 | 88 |
    89 | } 90 | {Object.keys(this.props.selectedBookError).length > 0 && 91 |
    92 | 93 |
    94 | } 95 | {Object.keys(this.props.deleteBook).length > 0 && 96 |
    97 | 98 |
    99 | } 100 |
    101 |
    102 |
    103 |
    104 |

    My Books Add Book

    105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | {this.renderBooks(this.handleModalShowClick)} 116 | 117 |
    #TitleISBN
    118 |
    119 |
    120 | 121 |
    122 | ); 123 | } 124 | } 125 | 126 | Book.propTypes = { 127 | books: PropTypes.array 128 | }; 129 | 130 | function mapStateToProps(state){ 131 | return { 132 | books: state.books.list_book.books, 133 | selectedBook: state.books.list_book.selectedBook, 134 | errors: state.books.list_book.errors, 135 | selectedBookError: state.books.list_book.selectedBookError, 136 | deleteBook: state.books.list_book.deleteBook 137 | }; 138 | } 139 | 140 | function mapDispatchToProps(dispatch){ 141 | return { 142 | getBooks: () => dispatch(bookPageInit()), 143 | getBookDetail: (id) => dispatch(bookDetailInit(id)), 144 | detailModalClose: () => dispatch(bookDetailClose()), 145 | deleteBookRequest: (id) => dispatch(bookDeleteInit(id)), 146 | releaseData: () => dispatch(releaseStateData()), 147 | }; 148 | } 149 | 150 | export default connect( 151 | mapStateToProps, 152 | mapDispatchToProps, 153 | )(withRouter(Book)); -------------------------------------------------------------------------------- /src/modules/Register/ConfirmAccount/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import {withRouter} from 'react-router' 4 | import { Formik, Field } from 'formik'; 5 | import * as Yup from 'yup'; 6 | import '../../Login/login.css'; 7 | import PropTypes from 'prop-types'; 8 | import { verifyOTPRequest,otpPageInit, clearConfirmData, resendOTPRequest } from './actions'; 9 | import FlashMessage from '../../../components/FlashMessage/FlashMessage'; 10 | import { browserRedirect } from '../../../helpers/helpers'; 11 | import Spinner from '../../../components/Spinner/Spinner'; 12 | 13 | class ConfirmAccount extends Component { 14 | constructor(props){ 15 | super(props); 16 | this.handleOTPResendBtn = this.handleOTPResendBtn.bind(this); 17 | } 18 | 19 | componentDidMount(){ 20 | if(!this.props.email){ 21 | browserRedirect('/'); 22 | } 23 | } 24 | 25 | handleOTPResendBtn(){ 26 | this.props.resendOTP(this.props.email); 27 | } 28 | 29 | componentWillUnmount(){ 30 | this.props.clearConfirmDetails(); 31 | } 32 | 33 | render(){ 34 | return( 35 |
    36 | {this.props.requesting && } 37 |
    38 |
    39 |
    40 | {Object.keys(this.props.errors).length > 0 && 41 |
    42 | 43 |
    44 | } 45 |
    46 |
    47 | {Object.keys(this.props.resend_success).length > 0 && 48 |
    49 | 50 |
    51 | } 52 |
    53 |
    54 |
    55 |
    56 |
    57 |

    Confirm Account

    58 | 66 | {props => { 67 | const { 68 | values, 69 | touched, 70 | errors, 71 | isValid, 72 | isSubmitting, 73 | handleChange, 74 | handleBlur, 75 | handleSubmit, 76 | } = props; 77 | return ( 78 |
    79 |
    80 | 81 | 91 | {errors.otp && touched.otp && ( 92 |
    {errors.otp}
    93 | )} 94 |
    95 | 96 |
    97 | 98 | 99 |
    100 | 101 | ); 102 | }} 103 |
    104 |
    105 |
    106 |
    107 | ); 108 | } 109 | } 110 | 111 | ConfirmAccount.propTypes = { 112 | onSubmitForm: PropTypes.func, 113 | errors: PropTypes.object 114 | }; 115 | 116 | function mapStateToProps(state){ 117 | return { 118 | errors: state.register.otp_errors, 119 | email: state.register.confirm_email, 120 | resend_success: state.register.resend_success, 121 | requesting: state.register.requesting 122 | }; 123 | } 124 | 125 | function mapDispatchToProps(dispatch) { 126 | return { 127 | onSubmitForm: (evt, actions) => { 128 | if (evt !== undefined && evt.preventDefault) evt.preventDefault(); 129 | dispatch(verifyOTPRequest(evt)); 130 | setTimeout(() => { 131 | actions.setSubmitting(false); 132 | }, 500); 133 | }, 134 | onPageInit: dispatch(otpPageInit()), 135 | clearConfirmDetails: () => dispatch(clearConfirmData()), 136 | resendOTP: (email) => dispatch(resendOTPRequest(email)) 137 | }; 138 | } 139 | 140 | export default connect( 141 | mapStateToProps, 142 | mapDispatchToProps, 143 | )(withRouter(ConfirmAccount)); -------------------------------------------------------------------------------- /src/modules/Login/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link, withRouter } from 'react-router-dom'; 4 | import { Formik } from 'formik'; 5 | import * as Yup from 'yup'; 6 | import './login.css'; 7 | import PropTypes from 'prop-types'; 8 | import { loginRequest,loginPageInit } from './actions'; 9 | import FlashMessage from '../../components/FlashMessage/FlashMessage'; 10 | import { redirectForConfirm } from '../Register/ConfirmAccount/actions'; 11 | import { browserRedirect } from '../../helpers/helpers'; 12 | 13 | 14 | class Login extends Component { 15 | constructor(props){ 16 | super(props); 17 | this.state = { 18 | email: '' 19 | } 20 | this.handleConfirmClick = this.handleConfirmClick.bind(this); 21 | this.handleLoginSubmit = this.handleLoginSubmit.bind(this); 22 | } 23 | 24 | messageForConfirm() { 25 | return ( 26 | 27 | You can confirm your account from . 28 | 29 | ); 30 | } 31 | 32 | handleConfirmClick(){ 33 | this.props.redirectForConfirm(this.state.email); 34 | browserRedirect('/confirm-account'); 35 | } 36 | 37 | handleLoginSubmit(email){ 38 | this.setState({email}) 39 | } 40 | 41 | render(){ 42 | let {errors} = this.props; 43 | let err_message = errors.data?errors.data:errors.message; 44 | let confirm_message = err_message === "Account is not confirmed. Please confirm your account."?true:false; 45 | return( 46 |
    47 |
    48 |
    49 | {Object.keys(errors).length > 0 && 50 |
    51 | 52 | {confirm_message && 53 |
    {this.messageForConfirm()}
    54 | } 55 |
    56 | } 57 |
    58 |
    59 |
    60 |
    61 |

    Login

    62 | 73 | {props => { 74 | const { 75 | values, 76 | touched, 77 | errors, 78 | isValid, 79 | handleChange, 80 | handleBlur, 81 | handleSubmit, 82 | } = props; 83 | return ( 84 |
    85 |
    86 | 87 | 97 | {errors.email && touched.email && ( 98 |
    {errors.email}
    99 | )} 100 | We'll never share your email with anyone else. 101 |
    102 |
    103 | 104 | 114 | {errors.password && touched.password && ( 115 |
    {errors.password}
    116 | )} 117 |
    118 | 121 |
    122 | Not registered yet? Register from here. 123 |
    124 |
    125 | ); 126 | }} 127 |
    128 |
    129 |
    130 |
    131 | ); 132 | } 133 | } 134 | 135 | Login.propTypes = { 136 | onSubmitForm: PropTypes.func, 137 | errors: PropTypes.object 138 | }; 139 | 140 | function mapStateToProps(state){ 141 | return { errors: state.login.errors}; 142 | } 143 | 144 | function mapDispatchToProps(dispatch) { 145 | return { 146 | onSubmitForm: evt => { 147 | if (evt !== undefined && evt.preventDefault) evt.preventDefault(); 148 | dispatch(loginRequest(evt)); 149 | }, 150 | onPageInit: dispatch(loginPageInit()), 151 | redirectForConfirm: email => dispatch(redirectForConfirm(email)) 152 | }; 153 | } 154 | 155 | export default connect( 156 | mapStateToProps, 157 | mapDispatchToProps, 158 | )(withRouter(Login)); -------------------------------------------------------------------------------- /src/modules/Book/ManageBook/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link, withRouter } from 'react-router-dom'; 4 | import { Formik } from 'formik'; 5 | import * as Yup from 'yup'; 6 | import '../book.css'; 7 | import FlashMessage from '../../../components/FlashMessage/FlashMessage'; 8 | import { bookAddPageInit, bookAddRequest, bookUpdateRequest } from './actions'; 9 | import { bookDetailInit } from '../actions'; 10 | import PropTypes from 'prop-types'; 11 | 12 | class ManageBook extends Component { 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | id: null, 18 | isEditing: false, 19 | } 20 | } 21 | 22 | componentDidMount(){ 23 | const { match: { params: {id} } } = this.props; 24 | if(id){ 25 | this.props.getBookDetail(id); 26 | this.setState({id:id, isEditing: true}); 27 | } 28 | } 29 | 30 | static getDerivedStateFromProps(props, state) { 31 | const { match: { params: {id} } } = props; 32 | if(id !== null && Object.keys(props.selectedBook).length === 0){ 33 | if(state.isEditing !== false){ 34 | return {id: null, isEditing: false}; 35 | } 36 | }else{ 37 | if(state.isEditing !== true){ 38 | return {id: id, isEditing: true}; 39 | } 40 | } 41 | return null; 42 | } 43 | 44 | render(){ 45 | let initialValues = {title:"",description:"",isbn:""}; 46 | if(Object.keys(this.props.selectedBook).length > 0) 47 | initialValues = this.props.selectedBook; 48 | return( 49 |
    50 |
    51 |
    52 | {Object.keys(this.props.errors).length > 0 && 53 |
    54 | 55 |
    56 | } 57 |
    58 |
    59 |
    60 |
    61 |

    { this.state.isEditing?"Update":"Add"} Book All Books

    62 | 75 | {props => { 76 | const { 77 | values, 78 | touched, 79 | errors, 80 | isValid, 81 | handleChange, 82 | handleBlur, 83 | handleSubmit, 84 | } = props; 85 | return ( 86 |
    87 |
    88 | 89 | 99 | {errors.title && touched.title && ( 100 |
    {errors.title}
    101 | )} 102 |
    103 |
    104 | 105 | 115 | {errors.isbn && touched.isbn && ( 116 |
    {errors.isbn}
    117 | )} 118 |
    119 |
    120 | 121 |