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