├── 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 |
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 |
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 |
27 | ×
28 |
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 |
9 | [Company-Name]
10 |
11 |
12 |
13 |
14 |
15 |
16 | Home
17 |
18 |
19 | My Books
20 |
21 |
22 |
23 |
26 |
27 | {/*
Profile */}
28 |
Logout
29 |
30 |
31 |
32 |
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 | You need to enable JavaScript to run this app.
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 |
56 | ×
57 |
58 |
59 |
60 | {this.showModelBody()}
61 |
62 |
63 | {this.props.isForDelete &&
64 | this.handleDeleteClick(this.props.selectedBook.id)}>Delete
65 | }
66 | Close
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 | [](https://www.linkedin.com/in/maitray-suthar/) [](https://github.com/maitraysuthar/react-redux-saga-boilerplate/blob/master/LICENSE) 
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 | View
67 | Update
68 | Delete
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 | Title
110 | ISBN
111 |
112 |
113 |
114 |
115 | {this.renderBooks(this.handleModalShowClick)}
116 |
117 |
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 |
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 here .
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 |
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 |
137 | );
138 | }}
139 |
140 |
141 |
142 |
143 | );
144 | }
145 | }
146 |
147 | ManageBook.propTypes = {
148 | onSubmitForm: PropTypes.func,
149 | errors: PropTypes.object
150 | };
151 |
152 | function mapStateToProps(state){
153 | return {
154 | errors: state.books.manage_book.errors,
155 | selectedBook: state.books.list_book.selectedBook
156 | };
157 | }
158 |
159 | function mapDispatchToProps(dispatch) {
160 | return {
161 | onSubmitForm: state => evt => {
162 | if (evt !== undefined && evt.preventDefault) evt.preventDefault();
163 | (state.isEditing) ? dispatch(bookUpdateRequest(evt,state.id)): dispatch(bookAddRequest(evt));
164 | },
165 | onPageInit: dispatch(bookAddPageInit()),
166 | getBookDetail: (id) => dispatch(bookDetailInit(id)),
167 | };
168 | }
169 |
170 | export default connect(
171 | mapStateToProps,
172 | mapDispatchToProps,
173 | )(withRouter(ManageBook));
--------------------------------------------------------------------------------
/src/modules/Register/index.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 | import {withRouter} from 'react-router'
5 | import { Formik } from 'formik';
6 | import * as Yup from 'yup';
7 | import '../Login/login.css';
8 | import PropTypes from 'prop-types';
9 | import { registerRequest,registerPageInit } from './actions';
10 | import FlashMessage from '../../components/FlashMessage/FlashMessage';
11 | import Spinner from '../../components/Spinner/Spinner';
12 |
13 | class Register extends Component {
14 | componentDidUpdate(prevProps, prevState) {
15 | // reset form
16 | if(Object.keys(this.props.user).length > 0){
17 | this.formik.resetForm();
18 | }
19 | }
20 | render(){
21 | return(
22 |
23 | {this.props.requesting &&
}
24 |
25 |
26 | {Object.keys(this.props.errors).length > 0 &&
27 |
28 |
29 |
30 | }
31 | {Object.keys(this.props.user).length > 0 &&
32 |
33 |
34 |
35 | }
36 |
37 |
38 |
39 |
40 |
Registration
41 |
this.formik = ref}
43 | initialValues={{ firstName: '',lastName: '', email: '', password:'', confirmPassword:'' }}
44 | onSubmit={this.props.onSubmitForm}
45 | validationSchema={Yup.object().shape({
46 | firstName: Yup.string().matches(/^[a-zA-Z]+$/,'First name only allows alphabets.')
47 | .required('First Name Required'),
48 | lastName: Yup.string()
49 | .required('Last Name Required'),
50 | email: Yup.string()
51 | .email()
52 | .required('Email Required'),
53 | password: Yup.string()
54 | .required('Password Required').min(6),
55 | confirmPassword: Yup.string()
56 | .oneOf([Yup.ref('password'), null], "Passwords don't match")
57 | .required('Confirm Password Required'),
58 | })}
59 | >
60 | {props => {
61 | const {
62 | values,
63 | touched,
64 | errors,
65 | isValid,
66 | isSubmitting,
67 | handleChange,
68 | handleBlur,
69 | handleSubmit,
70 | } = props;
71 | return (
72 |
73 |
74 |
First Name
75 |
85 | {errors.firstName && touched.firstName && (
86 |
{errors.firstName}
87 | )}
88 |
89 |
90 |
Last Name
91 |
101 | {errors.lastName && touched.lastName && (
102 |
{errors.lastName}
103 | )}
104 |
105 |
106 |
Email address
107 |
117 | {errors.email && touched.email && (
118 |
{errors.email}
119 | )}
120 |
We'll never share your email with anyone else.
121 |
122 |
123 |
Password
124 |
134 | {errors.password && touched.password && (
135 |
{errors.password}
136 | )}
137 |
138 |
139 |
Confirm Password
140 |
150 | {errors.confirmPassword && touched.confirmPassword && (
151 |
{errors.confirmPassword}
152 | )}
153 |
154 | Submit
155 |
156 | Already registered? Login from here.
157 |
158 |
159 | );
160 | }}
161 |
162 |
163 |
164 |
165 | );
166 | }
167 | }
168 |
169 | Register.propTypes = {
170 | onSubmitForm: PropTypes.func,
171 | errors: PropTypes.object
172 | };
173 |
174 | function mapStateToProps(state){
175 | return {
176 | errors: state.register.errors,
177 | user: state.register.user,
178 | requesting: state.register.requesting
179 | };
180 | }
181 |
182 | function mapDispatchToProps(dispatch) {
183 | return {
184 | onSubmitForm: (evt, actions) => {
185 | if (evt !== undefined && evt.preventDefault) evt.preventDefault();
186 | dispatch(registerRequest(evt));
187 | setTimeout(() => {
188 | actions.setSubmitting(false);
189 | }, 500);
190 | },
191 | onPageInit: dispatch(registerPageInit())
192 | };
193 | }
194 |
195 | export default connect(
196 | mapStateToProps,
197 | mapDispatchToProps,
198 | )(withRouter(Register));
--------------------------------------------------------------------------------