🔥 Major Update

* Adds Actions, Redicers and Middlewares
* Adds Http Service
* Adds Cancel option for Http Service
* Adds HOC's for API Loader, Sidebar and Headers
* Adds Random key generator for Routes
This commit is contained in:
Indrajith K L
2019-12-12 19:31:50 +05:30
parent f41d980fd8
commit 8883eacd2a
32 changed files with 662 additions and 51 deletions

View File

@@ -0,0 +1,16 @@
import { LOGIN_REQUEST, LOGIN_SUCCESS } from "../utils/constants";
export const loginRequest = (payload)=>{
return {
type: LOGIN_REQUEST,
...payload
};
}
export const loginSuccess = (payload)=>{
return {
type: LOGIN_SUCCESS,
...payload
};
}

View File

@@ -1,5 +1,6 @@
import AdminContainer from "../modules/admin/admin.container";
import SuperAdminContainer from "../modules/superadmin/superadmin.container";
import DashBoardContainer from "../modules/dashboard/dashboard.container";
export const AppRoutes = [
{
@@ -11,5 +12,10 @@ export const AppRoutes = [
path: '/superadmin',
component: SuperAdminContainer,
permission: ['admin', 'superadmin', 'user']
},
{
path: '/dashboard',
component: DashBoardContainer,
permission: ['user']
}
];

View File

@@ -1,6 +1,8 @@
import React from "react";
import { Route, Redirect } from "react-router-dom";
import Storage from "../services/storage.service";
import Permissions from "./permission.router";
import { RandomKey } from "../utils/random.key";
export const CustomRouter = ({ xComponent: Component, ...xProps }) => {
return (
@@ -19,7 +21,7 @@ export const CustomRouter = ({ xComponent: Component, ...xProps }) => {
return <Redirect to="/dashboard" />;
}
return <Component {...returnProps} />;
return <Permissions {...returnProps} key={RandomKey.generate()} Component={Component}/>;
}}
/>
);

View File

@@ -1,4 +1,19 @@
import React from "react";
import { Route, Redirect } from "react-router-dom";
// export const Permi
export const Permissions = ({Component: Component, ..._props})=>{
return (
<Route
render={props => {
let returnProps = {..._props, ...props};
// let token = Storage.get("token");
let pathName = props.match.path;
console.log(returnProps);
return <Component {...returnProps} />;
}}
/>
);
}
export default Permissions;

View File

@@ -1,15 +1,17 @@
import React, { Suspense } from "react";
import { Provider } from "react-redux";
import { Provider, connect } from "react-redux";
import { ConnectedRouter } from "connected-react-router";
import { Switch, Redirect, Route } from "react-router-dom";
import { Switch, Redirect, Route, withRouter } from "react-router-dom";
import { CustomRouter } from "./custom.router";
import LoginContainer from "../modules/login/login.container";
import DashBoardContainer from "../modules/dashboard/dashboard.container";
import { AppRoutes } from "./app.routes";
import MasterComponent from "../master/master.component";
const Routes = ({ store, history }) => {
return (
<Provider store={store}>
<MasterComponent>
<ConnectedRouter history={history}>
<Suspense
fallback={<div style={{ display: "none" }}> Loading ...</div>}
@@ -28,6 +30,7 @@ const Routes = ({ store, history }) => {
</Switch>
</Suspense>
</ConnectedRouter>
</MasterComponent>
</Provider>
);
};

View File

@@ -6,3 +6,18 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html, body {
margin:0;
padding:0;
height:100%;
}
.container-fluid{
height: 100%;
}
.content{
margin-left: 92px;
}

View File

@@ -11,6 +11,7 @@ import {
Route
} from "react-router-dom";
import Routes from './core/routes';
import HttpService from './services/http.service';
require("es6-promise").polyfill();
@@ -18,6 +19,9 @@ require("es6-promise").polyfill();
const store = configureStore();
store.runSaga(rootMiddleware);
HttpService.reduxStore = store;
HttpService.httpInterceptor();
// console.log(HttpService.httpInterceptor)
const XRouter = () => {
return (

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { createLoadingSelector, createNotificationSelector } from '../services/selectors';
import WithLoader from '../shared/loader.hoc';
import { connect } from "react-redux";
let loadingSelector = createLoadingSelector([
'LOGIN',
'COMMON'
]);
let errorSelector = createNotificationSelector([
'LOGIN',
'COMMON'
]);
const MasterComponent = (props) => {
return (
<>
{props.children}
</>
);
}
const mapStateToProps = state => {
return {
isLoading: loadingSelector(state),
error: errorSelector(state)
};
};
export default connect(mapStateToProps)(
WithLoader(MasterComponent)
);

View File

@@ -0,0 +1,29 @@
import { put, call, fork, takeEvery } from "redux-saga/effects";
import { LOGIN_REQUEST, LOGIN_SUCCESS } from "../utils/constants";
import { loginMock } from "../modules/login/login.service";
import { history } from '../core/store';
function* loginWatcher() {
yield takeEvery(LOGIN_REQUEST, loginWorker);
}
function* loginWorker(action) {
let { email, password } = action;
let res = yield call(loginApi, { email, password });
if (res && res.data) {
let { token } = res.data;
yield put({
type: LOGIN_SUCCESS,
payload: {
token
}
});
history.push('/dashboard');
}
console.log(res);
}
function loginApi(params) {
return loginMock(params);
}
export const LoginSaga = [fork(loginWatcher)];

View File

@@ -1,7 +1,8 @@
import { all } from "redux-saga/effects";
import { LoginSaga } from "./login.middleware";
export default function* rootMiddleware() {
yield all([
//...LoginSaga,
...LoginSaga,
]);
}

View File

@@ -1,5 +1,7 @@
import React, { Component } from 'react';
import { connect } from "react-redux";
import WithHeaderFooter from '../../shared/header_footer.hoc';
import WithSidebar from '../../shared/sidebar.hoc';
class AdminContainer extends Component {
constructor(props) {
console.log(props);
@@ -8,9 +10,24 @@ class AdminContainer extends Component{
render() {
return (
<div>Admin Container</div>
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
Admin Container
</div>
</div>
</div>
);
}
}
export default AdminContainer;
const mapStateToProps = state => {
return {
};
};
export default connect(mapStateToProps)(
WithSidebar(WithHeaderFooter(AdminContainer))
);

View File

@@ -1,12 +1,28 @@
import React, { Component } from 'react';
import { connect } from "react-redux";
import WithHeaderFooter from '../../shared/header_footer.hoc';
import WithSidebar from '../../shared/sidebar.hoc';
class DashBoardContainer extends Component {
render() {
return (
<div>Dashboard</div>
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
Dashboard
</div>
</div>
</div>
);
}
}
export default DashBoardContainer;
const mapStateToProps = state => {
return {
};
};
export default connect(mapStateToProps)(
WithSidebar(WithHeaderFooter(DashBoardContainer))
);

View File

@@ -1,12 +1,50 @@
import React, { Component } from 'react';
import { connect } from "react-redux";
import WithFooter from '../../shared/footer.hoc';
import { loginMock } from './login.service';
import { loginRequest } from '../../actions/login.action';
import HttpService from '../../services/http.service';
class LoginContainer extends Component {
state = {
};
onLogin = ()=>{
let params = {
email: "eve.holt@reqres.in",
password: "cityslicka"
};
// loginMock(params).then(res=>{
// console.log(res);
// })
this.props.dispatch(loginRequest(params));
// HttpService.cancelRequest();
}
render() {
return (
<div>Login</div>
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
Login
</div>
<div className="col-md-12">
<button className="btn btn-primary" onClick={this.onLogin}>Login</button>
</div>
</div>
</div>
);
}
}
export default LoginContainer;
const mapStateToProps = state => {
return {
};
};
export default connect(mapStateToProps)(
WithFooter(LoginContainer)
);

View File

@@ -0,0 +1,9 @@
import HttpService from '../../services/http.service';
export const loginMock = (params)=>{
return HttpService.fetch({
url: 'https://reqres.in/api/login',
method: 'post',
data: params
});
}

View File

@@ -1,12 +1,57 @@
import React, { Component } from 'react';
import { connect } from "react-redux";
import WithHeaderFooter from '../../shared/header_footer.hoc';
import WithSidebar from '../../shared/sidebar.hoc';
import { COMMON_REQUEST, COMMON_CANCEL } from '../../utils/constants';
import HttpService from '../../services/http.service';
class SuperAdminContainer extends Component {
componentDidMount() {
}
startRequest = () => {
this.props.dispatch({
type: COMMON_REQUEST
});
let params = {
url: 'https://reqres.in/api/users?page=2'
}
HttpService.fetch(params).then(res=>{
console.log(res);
})
}
stopRequest = () => {
// this.props.dispatch({
// type: COMMON_CANCEL
// })
HttpService.cancelRequest();
}
render() {
return (
<div>SuperAdminContainer</div>
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
SuperAdminContainer
</div>
</div>
<div className="row">
<div className="col-md-1"><button className="btn btn-primary" onClick={this.startRequest}>Start Request</button></div>
<div className="col-md-1"><button className="btn btn btn-danger" onClick={this.stopRequest}>Stop Request</button></div>
</div>
</div>
);
}
}
export default SuperAdminContainer;
const mapStateToProps = state => {
return {
};
};
export default connect(mapStateToProps)(
WithSidebar(WithHeaderFooter(SuperAdminContainer))
);

View File

@@ -0,0 +1,18 @@
export default function ErrorReducer(state = {}, action) {
const { type, error } = action;
const matches = /(.*)_(REQUEST|FAILED|ERROR)/.exec(
type
);
if (!matches) return state;
const [, requestName, requestState] = matches;
return {
errorMessage:
requestState === "FAILED" || requestState === "ERROR"
? error
? error
: ""
: "",
[requestName]: requestState === "FAILED" || requestState === "ERROR" ? true: false
};
}

View File

@@ -0,0 +1,12 @@
export default function LoadingReducer(state = {}, action) {
console.log("Reducer",action)
const { type } = action;
const matches = /(.*)_(REQUEST|SUCCESS|FAILED|ERROR|SUBMIT|CANCEL)/.exec(type);
if (!matches) return state;
const [, requestName, requestState] = matches;
return {
...state,
[requestName]: (requestState === 'REQUEST' || requestState === 'SUBMIT')
};
}

View File

@@ -0,0 +1,21 @@
import { LOGIN_REQUEST, LOGIN_SUCCESS } from "../utils/constants";
import Storage from "../services/storage.service";
let initialState = {
token: ""
};
export const LoginReducer = (state=initialState, action)=>{
switch (action.type) {
case LOGIN_REQUEST: return state;
case LOGIN_SUCCESS:
Storage.set('token', action.payload.token);
return {
...state,
token: action.payload.token
};
default: return state;
}
}

View File

@@ -1,9 +1,15 @@
import { combineReducers } from "redux";
import { connectRouter } from "connected-react-router";
import LoadingReducer from "./loading.reducer";
import ErrorReducer from "./error.reducer";
import { LoginReducer } from "./login.reducer";
const createRootReducer = history =>
combineReducers({
router: connectRouter(history),
loading: LoadingReducer,
error: ErrorReducer,
login: LoginReducer
});
export default createRootReducer;

View File

@@ -0,0 +1,90 @@
import axios from 'axios';
import Storage from './storage.service';
import { COMMON_REQUEST, COMMON_SUCCESS, COMMON_CANCEL } from '../utils/constants';
class HttpServiceSingleton {
constructor() {
axios.defaults.headers.post["Content-Type"] = "application/json";
axios.defaults.headers.put["Accept"] = "application/json";
this.count = 0; // This will store API requests
this.complete = 0; // This will store API Success/errors
}
set reduxStore(store) {
this.store = store;
}
fetch(payload, noAuth) {
const cancelToken = axios.CancelToken;
this.cancelSource = cancelToken.source();
let config = {};
if (!noAuth) {
let token = Storage.get("token");
config.headers = {
'Authirization': `Bearer ${token}`
};
}
config.method = payload.method ? payload.method : 'get';
config.url = payload.url ? payload.url : '';
config.cancelToken = this.cancelSource.token;
if (payload.data) config.data = JSON.stringify(payload.data);
return axios(config).catch((thrown) => {
if (axios.isCancel(thrown)) {
this.count = 0;
this.complete = 0;
this.store.dispatch({
type: COMMON_CANCEL
});
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
}
httpInterceptor() {
axios.interceptors.request.use((config) => {
this.count++;
this.checkApiComplete(this.store);
return config;
}, (error) => {
this.complete++;
this.checkApiComplete(this.store);
return Promise.reject(error);
});
axios.interceptors.response.use((response) => {
this.complete++;
this.checkApiComplete(this.store);
return response;
}, (error) => {
this.complete++;
this.checkApiComplete(this.store);
return Promise.reject(error);
});
}
cancelRequest() {
this.cancelSource.cancel();
}
//Fallback to cancel all API loaders in case of multiple API call occuts
checkApiComplete(store) {
if (this.count == this.complete) { // Cancel API loader when all requests are completed
store.dispatch({
type: COMMON_SUCCESS
});
} else {
store.dispatch({
type: COMMON_REQUEST
});
}
}
}
const HttpService = new HttpServiceSingleton();
// Object.freeze(HttpService); // Singleton Http Service
export default HttpService;

10
src/services/selectors.js Normal file
View File

@@ -0,0 +1,10 @@
import _ from "lodash";
export const createLoadingSelector = actions => state => {
return _(actions).some(action => _.get(state, `loading.${action}`));
};
export const createNotificationSelector = actions => state => {
return _(actions)
.some(action => _.get(state, `error.${action}`));
};

View File

@@ -9,7 +9,8 @@ class Storage{
if(!key||!value){
throw("Storag.set expects a 'key' and a 'value' - 'value' & 'key' can't be null");
}
localStorage.setItem(key, JSON.stringify(value));
value = (typeof value=="string") ? value : JSON.stringify(value);
localStorage.setItem(key, value);
}
}

13
src/shared/footer.css Normal file
View File

@@ -0,0 +1,13 @@
.footer {
position: absolute;
bottom: 0;
width: 95%;
height: 60px;
line-height: 60px;
background-color: #f5f5f5;
}
.footer>.container {
padding-right: 15px;
padding-left: 15px;
}

35
src/shared/footer.hoc.js Normal file
View File

@@ -0,0 +1,35 @@
import React, { Component } from "react";
import { Link } from 'react-router-dom';
import { connect } from "react-redux";
import { compose } from "redux";
import './footer.css';
const Footer = (HocComponent) => {
return class FooterComponent extends Component {
render() {
return (
<React.Fragment>
<HocComponent {...this.props} />
<footer className="footer">
<div className="container">Footer</div>
</footer>
</React.Fragment>
);
}
}
}
const mapStateToProps = state => {
return {
};
};
const WithFooter = compose(
connect(mapStateToProps, null),
Footer
)
export default WithFooter;

View File

@@ -0,0 +1,39 @@
import React, { Component } from "react";
import { Link } from 'react-router-dom';
import { connect } from "react-redux";
import { compose } from "redux";
const HeaderFooter = (HocComponent) => {
return class HeaderFooterComponent extends Component {
render() {
return (
<React.Fragment>
<nav className="nav">
<Link className="nav-link" to={"/dashboard"} >Dashboard</Link>
<Link className="nav-link" to={"/superadmin"} >Superadmin</Link>
<Link className="nav-link" to={"/admin"} >Admin</Link>
<Link className="nav-link" to={"/login"} >Login</Link>
</nav>
<HocComponent {...this.props} />
<footer className="footer">
<div className="container">Footer</div>
</footer>
</React.Fragment>
);
}
}
}
const mapStateToProps = state => {
return {
};
};
const WithHeaderFooter = compose(
connect(mapStateToProps, null),
HeaderFooter
)
export default WithHeaderFooter;

48
src/shared/loader.css Normal file
View File

@@ -0,0 +1,48 @@
.loader {
position: absolute;
z-index: 6;
top: 0px;
left: 0;
height: 4px;
width: 100%;
overflow: hidden;
background-color: #ddd;
}
.loader:before {
display: block;
position: absolute;
content: "";
left: -200px;
width: 200px;
height: 4px;
background-color: #ff8373;
animation: loading 2s linear infinite;
}
@keyframes loading {
from {
left: -200px;
width: 30%;
}
50% {
width: 30%;
}
70% {
width: 70%;
}
80% {
left: 50%;
}
95% {
left: 120%;
}
to {
left: 100%;
}
}

16
src/shared/loader.hoc.js Normal file
View File

@@ -0,0 +1,16 @@
import React from 'react';
import "./loader.css";
const WithLoader = HocComponent => {
return function ({ ...props }) {
return (
<div className="loader-container">
{props.isLoading && (
<div className="loader"></div>
)}
<HocComponent {...props} />
</div>
);
};
};
export default WithLoader;

6
src/shared/sidebar.css Normal file
View File

@@ -0,0 +1,6 @@
.sidebar{
background: #cccc;
width: 92px;
height: calc(100vh);
position: absolute;
}

36
src/shared/sidebar.hoc.js Normal file
View File

@@ -0,0 +1,36 @@
import React, { Component } from "react";
import { Link } from 'react-router-dom';
import { connect } from "react-redux";
import { compose } from "redux";
import './sidebar.css';
const Sidebar = (HocComponent) => {
return class SidebarComponent extends Component {
render() {
return (
<React.Fragment>
<div className="sidebar">
Sidebar
</div>
<div className="content">
<HocComponent {...this.props} />
</div>
</React.Fragment>
);
}
}
}
const mapStateToProps = state => {
return {
};
};
const WithSidebar = compose(
connect(mapStateToProps, null),
Sidebar
)
export default WithSidebar;

5
src/utils/constants.js Normal file
View File

@@ -0,0 +1,5 @@
export const LOGIN_REQUEST = "LOGIN_REQUEST";
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
export const COMMON_REQUEST = "COMMON_REQUEST";
export const COMMON_SUCCESS = "COMMON_SUCCESS";
export const COMMON_CANCEL = "COMMON_CANCEL";

3
src/utils/random.key.js Normal file
View File

@@ -0,0 +1,3 @@
export const RandomKey = {
generate : ()=> parseInt(Math.random()*Math.pow(10,12),10)
};