Convert all of the login components into hook based ones

This commit is contained in:
Dane Everitt 2019-06-22 16:45:51 -07:00
parent aabf9b8a70
commit 328347fab7
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
20 changed files with 435 additions and 598 deletions

View File

@ -2,11 +2,11 @@
"name": "pterodactyl-panel", "name": "pterodactyl-panel",
"dependencies": { "dependencies": {
"@hot-loader/react-dom": "^16.8.6", "@hot-loader/react-dom": "^16.8.6",
"@types/react-redux": "^7.0.9",
"axios": "^0.18.0", "axios": "^0.18.0",
"brace": "^0.11.1", "brace": "^0.11.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"date-fns": "^1.29.0", "date-fns": "^1.29.0",
"easy-peasy": "^2.5.0",
"feather-icons": "^4.10.0", "feather-icons": "^4.10.0",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"lodash": "^4.17.11", "lodash": "^4.17.11",
@ -14,12 +14,10 @@
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"react-hot-loader": "^4.9.0", "react-hot-loader": "^4.9.0",
"react-redux": "^7.1.0",
"react-router-dom": "^5.0.1", "react-router-dom": "^5.0.1",
"react-transition-group": "^4.1.0", "react-transition-group": "^4.1.0",
"redux": "^4.0.1",
"redux-persist": "^5.10.0",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"use-react-router": "^1.0.7",
"ws-wrapper": "^2.0.0", "ws-wrapper": "^2.0.0",
"xterm": "^3.5.1" "xterm": "^3.5.1"
}, },
@ -36,12 +34,10 @@
"@types/react-dom": "^16.8.4", "@types/react-dom": "^16.8.4",
"@types/react-router-dom": "^4.3.3", "@types/react-router-dom": "^4.3.3",
"@types/react-transition-group": "^2.9.2", "@types/react-transition-group": "^2.9.2",
"@types/redux-persist": "^4.3.1",
"@types/webpack-env": "^1.13.6", "@types/webpack-env": "^1.13.6",
"@typescript-eslint/eslint-plugin": "^1.10.1", "@typescript-eslint/eslint-plugin": "^1.10.1",
"@typescript-eslint/parser": "^1.10.1", "@typescript-eslint/parser": "^1.10.1",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^2.1.0", "css-loader": "^2.1.0",
"cssnano": "^4.0.3", "cssnano": "^4.0.3",
"eslint": "^5.16.0", "eslint": "^5.16.0",
@ -74,10 +70,11 @@
"webpack-manifest-plugin": "^2.0.3" "webpack-manifest-plugin": "^2.0.3"
}, },
"scripts": { "scripts": {
"clean": "rm -rf public/assets/*.js && rm -rf public/assets/*.css",
"watch": "NODE_ENV=development ./node_modules/.bin/webpack --watch --progress", "watch": "NODE_ENV=development ./node_modules/.bin/webpack --watch --progress",
"build": "NODE_ENV=development ./node_modules/.bin/webpack --progress", "build": "NODE_ENV=development ./node_modules/.bin/webpack --progress",
"build:production": "NODE_ENV=production ./node_modules/.bin/webpack", "build:production": "NODE_ENV=production ./node_modules/.bin/webpack",
"serve": "NODE_ENV=development webpack-dev-server --host 0.0.0.0 --hot", "serve": "yarn run clean && NODE_ENV=development webpack-dev-server --host 0.0.0.0 --hot",
"v:serve": "PUBLIC_PATH=http://pterodactyl.test:8080 yarn run serve" "v:serve": "PUBLIC_PATH=http://pterodactyl.test:8080 yarn run serve"
} }
} }

View File

@ -2,32 +2,27 @@ import * as React from 'react';
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
import { BrowserRouter as Router, Route } from 'react-router-dom'; import { BrowserRouter as Router, Route } from 'react-router-dom';
import AuthenticationRouter from '@/routers/AuthenticationRouter'; import AuthenticationRouter from '@/routers/AuthenticationRouter';
import { Provider } from 'react-redux';
import { persistor, store } from '@/redux/configure';
import { PersistGate } from 'redux-persist/integration/react';
import AccountRouter from '@/routers/AccountRouter'; import AccountRouter from '@/routers/AccountRouter';
import ServerOverviewContainer from '@/components/ServerOverviewContainer'; import ServerOverviewContainer from '@/components/ServerOverviewContainer';
import { StoreProvider } from 'easy-peasy';
import { store } from '@/state';
class App extends React.PureComponent { class App extends React.PureComponent {
render () { componentDidMount () {
return (
<Provider store={store}>
<PersistGate persistor={persistor} loading={this.renderLoading()}>
<Router basename={'/'}>
<div className={'mx-auto px-10 w-auto'} style={{ maxWidth: '1000px' }}>
<Route exact path="/" component={ServerOverviewContainer}/>
<Route path="/auth" component={AuthenticationRouter}/>
<Route path="/account" component={AccountRouter}/>
</div>
</Router>
</PersistGate>
</Provider>
);
} }
renderLoading () { render () {
return ( return (
<div className={'spinner spinner-lg'}></div> <StoreProvider store={store}>
<Router basename={'/'}>
<div className={'mx-auto px-10 w-auto'} style={{ maxWidth: '1000px' }}>
<Route exact path="/" component={ServerOverviewContainer}/>
<Route path="/auth" component={AuthenticationRouter}/>
<Route path="/account" component={AccountRouter}/>
</div>
</Router>
</StoreProvider>
); );
} }
} }

View File

@ -1,38 +1,33 @@
import * as React from 'react'; import React from 'react';
import { FlashMessage, ReduxState } from '@/redux/types';
import { connect } from 'react-redux';
import MessageBox from '@/components/MessageBox'; import MessageBox from '@/components/MessageBox';
import { State, useStoreState } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
type Props = Readonly<{ type Props = Readonly<{
spacerClass?: string; spacerClass?: string;
flashes: FlashMessage[]; withBottomSpace?: boolean;
}>; }>;
class FlashMessageRender extends React.PureComponent<Props> { export default ({ withBottomSpace, spacerClass }: Props) => {
render () { const flashes = useStoreState((state: State<ApplicationState>) => state.flashes.items);
if (this.props.flashes.length === 0) {
return null;
}
return ( if (flashes.length === 0) {
<React.Fragment> return null;
{
this.props.flashes.map((flash, index) => (
<React.Fragment key={flash.id || flash.type + flash.message}>
{index > 0 && <div className={this.props.spacerClass || 'mt-2'}></div>}
<MessageBox type={flash.type} title={flash.title}>
{flash.message}
</MessageBox>
</React.Fragment>
))
}
</React.Fragment>
);
} }
}
const mapStateToProps = (state: ReduxState) => ({ // noinspection PointlessBooleanExpressionJS
flashes: state.flashes, return (
}); <div className={withBottomSpace === false ? undefined : 'mb-2'}>
{
export default connect(mapStateToProps)(FlashMessageRender); flashes.map((flash, index) => (
<React.Fragment key={flash.id || flash.type + flash.message}>
{index > 0 && <div className={spacerClass || 'mt-2'}></div>}
<MessageBox type={flash.type} title={flash.title}>
{flash.message}
</MessageBox>
</React.Fragment>
))
}
</div>
);
};

View File

@ -1,109 +1,78 @@
import * as React from 'react'; import * as React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail'; import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
import { connect } from 'react-redux';
import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer'; import LoginFormContainer from '@/components/auth/LoginFormContainer';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import FlashMessageRender from '@/components/FlashMessageRender';
type Props = Readonly<{ export default () => {
pushFlashMessage: typeof pushFlashMessage; const [ isSubmitting, setSubmitting ] = React.useState(false);
clearAllFlashMessages: typeof clearAllFlashMessages; const [ email, setEmail ] = React.useState('');
}>;
type State = Readonly<{ const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
email: string;
isSubmitting: boolean;
}>;
class ForgotPasswordContainer extends React.PureComponent<Props, State> { const handleFieldUpdate = (e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value);
emailField = React.createRef<HTMLInputElement>();
state: State = { const handleSubmission = (e: React.FormEvent<HTMLFormElement>) => {
email: '',
isSubmitting: false,
};
handleFieldUpdate = (e: React.ChangeEvent<HTMLInputElement>) => this.setState({
email: e.target.value,
});
handleSubmission = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
this.setState({ isSubmitting: true }, () => { setSubmitting(true);
this.props.clearAllFlashMessages(); clearFlashes();
requestPasswordResetEmail(this.state.email) requestPasswordResetEmail(email)
.then(response => { .then(response => {
if (this.emailField.current) { setEmail('');
this.emailField.current.value = ''; addFlash({ type: 'success', title: 'Success', message: response });
} })
.catch(error => {
this.props.pushFlashMessage({ console.error(error);
type: 'success', title: 'Success', message: response, addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
}); })
}) .then(() => setSubmitting(false));
.catch(error => {
console.error(error);
this.props.pushFlashMessage({
type: 'error',
title: 'Error',
message: httpErrorToHuman(error),
});
})
.then(() => this.setState({ isSubmitting: false }));
});
}; };
render () { return (
return ( <div>
<div> <h2 className={'text-center text-neutral-100 font-medium py-4'}>
<h2 className={'text-center text-neutral-100 font-medium py-4'}> Request Password Reset
Request Password Reset </h2>
</h2> <FlashMessageRender/>
<LoginFormContainer onSubmit={this.handleSubmission}> <LoginFormContainer onSubmit={handleSubmission}>
<label htmlFor={'email'}>Email</label> <label htmlFor={'email'}>Email</label>
<input <input
ref={this.emailField} id={'email'}
id={'email'} type={'email'}
type={'email'} required={true}
required={true} className={'input'}
className={'input'} value={email}
onChange={this.handleFieldUpdate} onChange={handleFieldUpdate}
autoFocus={true} autoFocus={true}
/> />
<p className={'input-help'}> <p className={'input-help'}>
Enter your account email address to receive instructions on resetting your password. Enter your account email address to receive instructions on resetting your password.
</p> </p>
<div className={'mt-6'}> <div className={'mt-6'}>
<button <button
className={'btn btn-primary btn-jumbo flex justify-center'} className={'btn btn-primary btn-jumbo flex justify-center'}
disabled={this.state.isSubmitting || this.state.email.length < 5} disabled={isSubmitting || email.length < 5}
> >
{this.state.isSubmitting ? {isSubmitting ?
<div className={'spinner-circle spinner-sm spinner-white'}></div> <div className={'spinner-circle spinner-sm spinner-white'}></div>
: :
'Send Email' 'Send Email'
} }
</button> </button>
</div> </div>
<div className={'mt-6 text-center'}> <div className={'mt-6 text-center'}>
<Link <Link
to={'/login'} to={'/login'}
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'} className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
> >
Return to Login Return to Login
</Link> </Link>
</div> </div>
</LoginFormContainer> </LoginFormContainer>
</div> </div>
); );
}
}
const mapDispatchToProps = {
pushFlashMessage,
clearAllFlashMessages,
}; };
export default connect(null, mapDispatchToProps)(ForgotPasswordContainer);

View File

@ -1,112 +1,91 @@
import * as React from 'react'; import React, { useState } from 'react';
import { RouteComponentProps, StaticContext } from 'react-router';
import { connect } from 'react-redux';
import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash';
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
import MessageBox from '@/components/MessageBox';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import loginCheckpoint from '@/api/auth/loginCheckpoint'; import loginCheckpoint from '@/api/auth/loginCheckpoint';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer'; import LoginFormContainer from '@/components/auth/LoginFormContainer';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import useRouter from 'use-react-router';
import { StaticContext } from 'react-router';
import FlashMessageRender from '@/components/FlashMessageRender';
type State = Readonly<{ export default () => {
isLoading: boolean; const [ code, setCode ] = useState('');
errorMessage?: string; const [ isLoading, setIsLoading ] = useState(false);
code: string;
}>;
class LoginCheckpointContainer extends React.PureComponent<RouteComponentProps<{}, StaticContext, { token: string }>, State> { const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
state: State = { const { history, location: { state } } = useRouter<{}, StaticContext, { token?: string }>();
code: '',
isLoading: false,
};
componentDidMount () { if (!state || !state.token) {
const { state } = this.props.location; return history.replace('/login');
if (!state || !state.token) {
this.props.history.replace('/login');
}
} }
onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => { const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value.length > 6) { if (e.target.value.length <= 6) {
e.target.value = e.target.value.substring(0, 6); setCode(e.target.value);
return e.preventDefault();
} }
this.setState({ code: e.target.value });
}; };
submit = (e: React.FormEvent<HTMLFormElement>) => { const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
this.setState({ isLoading: true }, () => { setIsLoading(true);
loginCheckpoint(this.props.location.state.token, this.state.code) clearFlashes();
.then(response => {
if (response.complete) { loginCheckpoint(state.token!, code)
// @ts-ignore .then(response => {
window.location = response.intended || '/'; if (response.complete) {
} // @ts-ignore
}) window.location = response.intended || '/';
.catch(error => { }
console.error(error); })
this.setState({ errorMessage: httpErrorToHuman(error), isLoading: false }); .catch(error => {
}); console.error(error);
}); addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
setIsLoading(false);
});
}; };
render () { return (
return ( <React.Fragment>
<React.Fragment> <h2 className={'text-center text-neutral-100 font-medium py-4'}>
<h2 className={'text-center text-neutral-100 font-medium py-4'}> Device Checkpoint
Device Checkpoint </h2>
</h2> <FlashMessageRender/>
<NetworkErrorMessage message={this.state.errorMessage}/> <LoginFormContainer onSubmit={submit}>
<LoginFormContainer onSubmit={this.submit}> <div className={'mt-6'}>
<MessageBox type={'warning'}> <label htmlFor={'authentication_code'}>Authentication Code</label>
This account is protected with two-factor authentication. A valid authentication token must <input
be provided in order to continue. id={'authentication_code'}
</MessageBox> type={'number'}
<div className={'mt-6'}> autoFocus={true}
<label htmlFor={'authentication_code'}>Authentication Code</label> className={'input'}
<input value={code}
id={'authentication_code'} onChange={onChangeHandler}
type={'number'} />
autoFocus={true} </div>
className={'input'} <div className={'mt-6'}>
onChange={this.onChangeHandler} <button
/> type={'submit'}
</div> className={'btn btn-primary btn-jumbo'}
<div className={'mt-6'}> disabled={isLoading || code.length !== 6}
<button >
type={'submit'} {isLoading ?
className={'btn btn-primary btn-jumbo'} <span className={'spinner white'}>&nbsp;</span>
disabled={this.state.isLoading || this.state.code.length !== 6} :
> 'Continue'
{this.state.isLoading ? }
<span className={'spinner white'}>&nbsp;</span> </button>
: </div>
'Continue' <div className={'mt-6 text-center'}>
} <Link
</button> to={'/login'}
</div> className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
<div className={'mt-6 text-center'}> >
<Link Return to Login
to={'/login'} </Link>
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'} </div>
> </LoginFormContainer>
Return to Login </React.Fragment>
</Link> );
</div>
</LoginFormContainer>
</React.Fragment>
);
}
}
const mapDispatchToProps = {
pushFlashMessage,
clearAllFlashMessages,
}; };
export default connect(null, mapDispatchToProps)(LoginCheckpointContainer);

View File

@ -1,111 +1,96 @@
import * as React from 'react'; import React, { useState } from 'react';
import { Link, RouteComponentProps } from 'react-router-dom'; import { Link } from 'react-router-dom';
import login from '@/api/auth/login'; import login from '@/api/auth/login';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
import LoginFormContainer from '@/components/auth/LoginFormContainer'; import LoginFormContainer from '@/components/auth/LoginFormContainer';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
import useRouter from 'use-react-router';
type State = Readonly<{ export default () => {
errorMessage?: string; const [ username, setUsername ] = useState('');
isLoading: boolean; const [ password, setPassword ] = useState('');
username?: string; const [ isLoading, setLoading ] = useState(false);
password?: string; const { history } = useRouter();
}>;
export default class LoginContainer extends React.PureComponent<RouteComponentProps, State> { const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
state: State = {
isLoading: false,
};
submit = (e: React.FormEvent<HTMLFormElement>) => { const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
const { username, password } = this.state; setLoading(true);
clearFlashes();
this.setState({ isLoading: true }, () => { login(username!, password!)
login(username!, password!) .then(response => {
.then(response => { if (response.complete) {
if (response.complete) { // @ts-ignore
// @ts-ignore window.location = response.intended || '/';
window.location = response.intended || '/'; return;
return; }
}
this.props.history.replace('/login/checkpoint', { history.replace('/login/checkpoint', { token: response.confirmationToken });
token: response.confirmationToken, })
}); .catch(error => {
}) console.error(error);
.catch(error => this.setState({
isLoading: false, setLoading(false);
errorMessage: httpErrorToHuman(error), addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
}, () => console.error(error))); });
});
}; };
canSubmit () { const canSubmit = () => username && password && username.length > 0 && password.length > 0;
if (!this.state.username || !this.state.password) {
return false;
}
return this.state.username.length > 0 && this.state.password.length > 0; return (
} <React.Fragment>
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
// @ts-ignore Login to Continue
handleFieldUpdate = (e: React.ChangeEvent<HTMLInputElement>) => this.setState({ </h2>
[e.target.id]: e.target.value, <FlashMessageRender/>
}); <LoginFormContainer onSubmit={submit}>
<label htmlFor={'username'}>Username or Email</label>
render () { <input
return ( id={'username'}
<React.Fragment> autoFocus={true}
<h2 className={'text-center text-neutral-100 font-medium py-4'}> required={true}
Login to Continue className={'input'}
</h2> onChange={e => setUsername(e.target.value)}
<NetworkErrorMessage message={this.state.errorMessage}/> disabled={isLoading}
<LoginFormContainer onSubmit={this.submit}> />
<label htmlFor={'username'}>Username or Email</label> <div className={'mt-6'}>
<label htmlFor={'password'}>Password</label>
<input <input
id={'username'} id={'password'}
autoFocus={true}
required={true} required={true}
type={'password'}
className={'input'} className={'input'}
onChange={this.handleFieldUpdate} onChange={e => setPassword(e.target.value)}
disabled={this.state.isLoading} disabled={isLoading}
/> />
<div className={'mt-6'}> </div>
<label htmlFor={'password'}>Password</label> <div className={'mt-6'}>
<input <button
id={'password'} type={'submit'}
required={true} className={'btn btn-primary btn-jumbo'}
type={'password'} disabled={isLoading || !canSubmit()}
className={'input'} >
onChange={this.handleFieldUpdate} {isLoading ?
disabled={this.state.isLoading} <span className={'spinner white'}>&nbsp;</span>
/> :
</div> 'Login'
<div className={'mt-6'}> }
<button </button>
type={'submit'} </div>
className={'btn btn-primary btn-jumbo'} <div className={'mt-6 text-center'}>
disabled={this.state.isLoading || !this.canSubmit()} <Link
> to={'/password'}
{this.state.isLoading ? className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
<span className={'spinner white'}>&nbsp;</span> >
: Forgot password?
'Login' </Link>
} </div>
</button> </LoginFormContainer>
</div> </React.Fragment>
<div className={'mt-6 text-center'}> );
<Link };
to={'/password'}
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
>
Forgot password?
</Link>
</div>
</LoginFormContainer>
</React.Fragment>
);
}
}

View File

@ -8,7 +8,7 @@ export default ({ className, ...props }: React.DetailedHTMLProps<React.FormHTMLA
paddingLeft: 0, paddingLeft: 0,
}} }}
> >
<div className={'flex-none'}> <div className={'flex-none select-none'}>
<img src={'/assets/pterodactyl.svg'} className={'w-64'}/> <img src={'/assets/pterodactyl.svg'} className={'w-64'}/>
</div> </div>
<div className={'flex-1'}> <div className={'flex-1'}>

View File

@ -1,151 +1,109 @@
import * as React from 'react'; import React, { useState } from 'react';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { parse } from 'query-string'; import { parse } from 'query-string';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
import performPasswordReset from '@/api/auth/performPasswordReset'; import performPasswordReset from '@/api/auth/performPasswordReset';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import { connect } from 'react-redux';
import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash';
import LoginFormContainer from '@/components/auth/LoginFormContainer'; import LoginFormContainer from '@/components/auth/LoginFormContainer';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
type State = Readonly<{ type Props = Readonly<RouteComponentProps<{ token: string }> & {}>;
email?: string;
password?: string;
passwordConfirm?: string;
isLoading: boolean;
errorMessage?: string;
}>;
type Props = Readonly<RouteComponentProps<{ token: string }> & { export default (props: Props) => {
pushFlashMessage: typeof pushFlashMessage; const [ isLoading, setIsLoading ] = useState(false);
clearAllFlashMessages: typeof clearAllFlashMessages; const [ email, setEmail ] = useState('');
}>; const [ password, setPassword ] = useState('');
const [ passwordConfirm, setPasswordConfirm ] = useState('');
class ResetPasswordContainer extends React.PureComponent<Props, State> { const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
state: State = {
isLoading: false,
};
componentDidMount () { const parsed = parse(props.location.search);
const parsed = parse(this.props.location.search); if (email.length === 0 && parsed.email) {
setEmail(parsed.email as string);
this.setState({ email: parsed.email as string || undefined });
} }
canSubmit () { const canSubmit = () => password && email && password.length >= 8 && password === passwordConfirm;
if (!this.state.password || !this.state.email) {
return false;
}
return this.state.password.length >= 8 && this.state.password === this.state.passwordConfirm; const submit = (e: React.FormEvent<HTMLFormElement>) => {
}
onPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => this.setState({
password: e.target.value,
});
onPasswordConfirmChange = (e: React.ChangeEvent<HTMLInputElement>) => this.setState({
passwordConfirm: e.target.value,
});
onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
const { password, passwordConfirm, email } = this.state;
if (!password || !email || !passwordConfirm) { if (!password || !email || !passwordConfirm) {
return; return;
} }
this.props.clearAllFlashMessages(); setIsLoading(true);
this.setState({ isLoading: true }, () => { clearFlashes();
performPasswordReset(email, {
token: this.props.match.params.token,
password: password,
passwordConfirmation: passwordConfirm,
})
.then(response => {
if (response.redirectTo) {
// @ts-ignore
window.location = response.redirectTo;
return;
}
this.props.pushFlashMessage({ performPasswordReset(email, {
type: 'success', token: props.match.params.token, password, passwordConfirmation: passwordConfirm,
message: 'Your password has been reset, please login to continue.', })
}); .then(() => {
this.props.history.push('/login'); addFlash({ type: 'success', message: 'Your password has been reset, please login to continue.' });
}) props.history.push('/login');
.catch(error => { })
console.error(error); .catch(error => {
this.setState({ errorMessage: httpErrorToHuman(error) }); console.error(error);
}) addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
.then(() => this.setState({ isLoading: false })); })
}); .then(() => setIsLoading(false));
}; };
render () { return (
return ( <div>
<div> <h2 className={'text-center text-neutral-100 font-medium py-4'}>
<h2 className={'text-center text-neutral-100 font-medium py-4'}> Reset Password
Reset Password </h2>
</h2> <FlashMessageRender/>
<NetworkErrorMessage message={this.state.errorMessage}/> <LoginFormContainer onSubmit={submit}>
<LoginFormContainer onSubmit={this.onSubmit}> <label>Email</label>
<label>Email</label> <input className={'input'} value={email} disabled={true}/>
<input value={this.state.email || ''} disabled={true}/> <div className={'mt-6'}>
<div className={'mt-6'}> <label htmlFor={'new_password'}>New Password</label>
<label htmlFor={'new_password'}>New Password</label> <input
<input id={'new_password'}
id={'new_password'} className={'input'}
type={'password'} type={'password'}
required={true} required={true}
onChange={this.onPasswordChange} onChange={e => setPassword(e.target.value)}
/> />
<p className={'input-help'}> <p className={'input-help'}>
Passwords must be at least 8 characters in length. Passwords must be at least 8 characters in length.
</p> </p>
</div> </div>
<div className={'mt-6'}> <div className={'mt-6'}>
<label htmlFor={'new_password_confirm'}>Confirm New Password</label> <label htmlFor={'new_password_confirm'}>Confirm New Password</label>
<input <input
id={'new_password_confirm'} id={'new_password_confirm'}
type={'password'} className={'input'}
required={true} type={'password'}
onChange={this.onPasswordConfirmChange} required={true}
/> onChange={e => setPasswordConfirm(e.target.value)}
</div> />
<div className={'mt-6'}> </div>
<button <div className={'mt-6'}>
type={'submit'} <button
className={'btn btn-primary btn-jumbo'} type={'submit'}
disabled={this.state.isLoading || !this.canSubmit()} className={'btn btn-primary btn-jumbo'}
> disabled={isLoading || !canSubmit()}
{this.state.isLoading ? >
<span className={'spinner white'}>&nbsp;</span> {isLoading ?
: <span className={'spinner white'}>&nbsp;</span>
'Reset Password' :
} 'Reset Password'
</button> }
</div> </button>
<div className={'mt-6 text-center'}> </div>
<Link <div className={'mt-6 text-center'}>
to={'/login'} <Link
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'} to={'/login'}
> className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
Return to Login >
</Link> Return to Login
</div> </Link>
</LoginFormContainer> </div>
</div> </LoginFormContainer>
); </div>
} );
}
const mapDispatchToProps = {
pushFlashMessage,
clearAllFlashMessages,
}; };
export default connect(null, mapDispatchToProps)(ResetPasswordContainer);

View File

@ -1,17 +0,0 @@
import { FlashMessage } from '@/redux/types';
export const PUSH_FLASH_MESSAGE = 'PUSH_FLASH_MESSAGE';
export const REMOVE_FLASH_MESSAGE = 'REMOVE_FLASH_MESSAGE';
export const CLEAR_ALL_FLASH_MESSAGES = 'CLEAR_ALL_FLASH_MESSAGES';
export const pushFlashMessage = (payload: FlashMessage) => ({
type: PUSH_FLASH_MESSAGE, payload,
});
export const removeFlashMessage = (id: string) => ({
type: REMOVE_FLASH_MESSAGE, payload: id,
});
export const clearAllFlashMessages = () => ({
type: CLEAR_ALL_FLASH_MESSAGES,
});

View File

@ -1,14 +0,0 @@
import { createStore } from 'redux';
import { persistStore, persistReducer, PersistConfig } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { reducers } from './reducers';
const persistConfig: PersistConfig = {
key: 'root',
storage,
};
const persistedReducer = persistReducer(persistConfig, reducers);
export const store = createStore(persistedReducer);
export const persistor = persistStore(store);

View File

@ -1,7 +0,0 @@
import { combineReducers } from 'redux';
import flashReducer from './reducers/flash';
import { ReduxState } from '@/redux/types';
export const reducers = combineReducers<ReduxState>({
flashes: flashReducer,
});

View File

@ -1,21 +0,0 @@
import { FlashMessage, ReduxReducerAction } from '@/redux/types';
import { CLEAR_ALL_FLASH_MESSAGES, PUSH_FLASH_MESSAGE, REMOVE_FLASH_MESSAGE } from '@/redux/actions/flash';
export default (state: FlashMessage[] = [], action: ReduxReducerAction) => {
switch (action.type) {
case PUSH_FLASH_MESSAGE:
return [ ...state.filter(flash => {
if (action.payload.id && flash.id) {
return flash.id !== action.payload.id;
}
return true;
}), action.payload ];
case REMOVE_FLASH_MESSAGE:
return [ ...state.filter(flash => flash.id !== action.payload) ];
case CLEAR_ALL_FLASH_MESSAGES:
return [];
default:
return [ ...state ];
}
};

View File

@ -1,17 +0,0 @@
import { FlashMessageType } from '@/components/MessageBox';
export interface ReduxReducerAction {
type: string;
payload?: any;
}
export interface FlashMessage {
id?: string;
type: FlashMessageType;
title?: string;
message: string;
}
export interface ReduxState {
flashes: FlashMessage[];
}

View File

@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { CSSTransition, TransitionGroup } from 'react-transition-group';
import FlashMessageRender from '@/components/FlashMessageRender';
import DesignElements from '@/components/account/DesignElements'; import DesignElements from '@/components/account/DesignElements';
export default class AccountRouter extends React.PureComponent { export default class AccountRouter extends React.PureComponent {
@ -13,7 +12,6 @@ export default class AccountRouter extends React.PureComponent {
<TransitionGroup className={'route-transition-group'}> <TransitionGroup className={'route-transition-group'}>
<CSSTransition key={location.key} timeout={150} classNames={'fade'}> <CSSTransition key={location.key} timeout={150} classNames={'fade'}>
<section> <section>
<FlashMessageRender/>
<Switch location={location}> <Switch location={location}>
<Route path={'/'} component={DesignElements} exact/> <Route path={'/'} component={DesignElements} exact/>
<Route path={'/design'} component={DesignElements} exact/> <Route path={'/design'} component={DesignElements} exact/>

View File

@ -3,7 +3,6 @@ import { BrowserRouter, Route, Switch } from 'react-router-dom';
import LoginContainer from '@/components/auth/LoginContainer'; import LoginContainer from '@/components/auth/LoginContainer';
import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { CSSTransition, TransitionGroup } from 'react-transition-group';
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer'; import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
import FlashMessageRender from '@/components/FlashMessageRender';
import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer'; import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer'; import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
@ -16,7 +15,6 @@ export default class AuthenticationRouter extends React.PureComponent {
<TransitionGroup className={'route-transition-group mt-32'}> <TransitionGroup className={'route-transition-group mt-32'}>
<CSSTransition key={location.key} timeout={150} classNames={'fade'}> <CSSTransition key={location.key} timeout={150} classNames={'fade'}>
<section> <section>
<FlashMessageRender/>
<Switch location={location}> <Switch location={location}>
<Route path={'/login'} component={LoginContainer} exact/> <Route path={'/login'} component={LoginContainer} exact/>
<Route path={'/login/checkpoint'} component={LoginCheckpointContainer}/> <Route path={'/login/checkpoint'} component={LoginCheckpointContainer}/>

View File

@ -0,0 +1,16 @@
import { action, createStore } from 'easy-peasy';
import { ApplicationState } from '@/state/types';
const state: ApplicationState = {
flashes: {
items: [],
addFlash: action((state, payload) => {
state.items.push(payload);
}),
clearFlashes: action(state => {
state.items = [];
}),
},
};
export const store = createStore(state);

19
resources/scripts/state/types.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
import { FlashMessageType } from '@/components/MessageBox';
import { Action } from 'easy-peasy';
export interface ApplicationState {
flashes: FlashState;
}
export interface FlashState {
items: FlashMessage[];
addFlash: Action<FlashState, FlashMessage>;
clearFlashes: Action<FlashState>;
}
export interface FlashMessage {
id?: string;
type: FlashMessageType;
title?: string;
message: string;
}

View File

@ -6,6 +6,7 @@
"strict": true, "strict": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"moduleResolution": "node", "moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"lib": ["es2015", "dom"], "lib": ["es2015", "dom"],

View File

@ -4,7 +4,6 @@ const tailwind = require('tailwindcss');
const glob = require('glob-all'); const glob = require('glob-all');
const AssetsManifestPlugin = require('webpack-assets-manifest'); const AssetsManifestPlugin = require('webpack-assets-manifest');
const CleanPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PurgeCssPlugin = require('purgecss-webpack-plugin'); const PurgeCssPlugin = require('purgecss-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
@ -13,7 +12,6 @@ const TerserPlugin = require('terser-webpack-plugin');
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
let plugins = [ let plugins = [
new CleanPlugin(path.resolve(__dirname, 'public/assets')),
new MiniCssExtractPlugin({ filename: isProduction ? 'bundle.[chunkhash:8].css' : 'bundle.[hash:8].css' }), new MiniCssExtractPlugin({ filename: isProduction ? 'bundle.[chunkhash:8].css' : 'bundle.[hash:8].css' }),
new AssetsManifestPlugin({ new AssetsManifestPlugin({
writeToDisk: true, writeToDisk: true,

101
yarn.lock
View File

@ -605,7 +605,7 @@
"@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-self" "^7.0.0"
"@babel/plugin-transform-react-jsx-source" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.5": "@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0":
version "7.4.5" version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
dependencies: dependencies:
@ -751,13 +751,6 @@
version "4.7.2" version "4.7.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220"
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/lodash@^4.14.119": "@types/lodash@^4.14.119":
version "4.14.119" version "4.14.119"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39"
@ -778,15 +771,6 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-redux@^7.0.9":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.0.9.tgz#4825ee2872c44768916304b6bb8df5b46d443b88"
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-router-dom@^4.3.3": "@types/react-router-dom@^4.3.3":
version "4.3.3" version "4.3.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.3.tgz#7837e3e9fefbc84a8f6c8a51dca004f4e83e94e3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.3.tgz#7837e3e9fefbc84a8f6c8a51dca004f4e83e94e3"
@ -815,12 +799,6 @@
"@types/prop-types" "*" "@types/prop-types" "*"
csstype "^2.2.0" csstype "^2.2.0"
"@types/redux-persist@^4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@types/redux-persist/-/redux-persist-4.3.1.tgz#aa4c876859e0bea5155e5f7980e5b8c4699dc2e6"
dependencies:
redux-persist "*"
"@types/webpack-env@^1.13.6": "@types/webpack-env@^1.13.6":
version "1.13.6" version "1.13.6"
resolved "http://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.13.6.tgz#128d1685a7c34d31ed17010fc87d6a12c1de6976" resolved "http://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.13.6.tgz#128d1685a7c34d31ed17010fc87d6a12c1de6976"
@ -1908,12 +1886,6 @@ clean-css@4.1.x:
dependencies: dependencies:
source-map "0.5.x" source-map "0.5.x"
clean-webpack-plugin@^0.1.19:
version "0.1.19"
resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-0.1.19.tgz#ceda8bb96b00fe168e9b080272960d20fdcadd6d"
dependencies:
rimraf "^2.6.1"
cli-cursor@^2.1.0: cli-cursor@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
@ -2671,6 +2643,18 @@ duplexify@^3.4.2, duplexify@^3.6.0:
readable-stream "^2.0.0" readable-stream "^2.0.0"
stream-shift "^1.0.0" stream-shift "^1.0.0"
easy-peasy@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/easy-peasy/-/easy-peasy-2.5.0.tgz#5366ff59c71c980cc31ec230dc8855f72b8d2875"
dependencies:
immer "^3.1.3"
memoizerific "^1.11.3"
redux "^4.0.1"
redux-thunk "^2.3.0"
shallowequal "^1.1.0"
type-zoo "^3.3.0"
typelevel-ts "^0.3.5"
ee-first@1.1.1: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -3773,6 +3757,10 @@ ignore@^5.1.1:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.2.tgz#e28e584d43ad7e92f96995019cc43b9e1ac49558" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.2.tgz#e28e584d43ad7e92f96995019cc43b9e1ac49558"
immer@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/immer/-/immer-3.1.3.tgz#59bc742b2aab6e2c676445edb653e588a23c70fc"
import-cwd@^2.0.0: import-cwd@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@ -3884,7 +3872,7 @@ interpret@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
invariant@^2.2.0, invariant@^2.2.2, invariant@^2.2.4: invariant@^2.2.0, invariant@^2.2.2:
version "2.2.4" version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies: dependencies:
@ -4535,6 +4523,10 @@ map-cache@^0.2.2:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
map-or-similar@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08"
map-visit@^1.0.0: map-visit@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@ -4570,6 +4562,12 @@ mem@^4.0.0:
mimic-fn "^1.0.0" mimic-fn "^1.0.0"
p-is-promise "^2.0.0" p-is-promise "^2.0.0"
memoizerific@^1.11.3:
version "1.11.3"
resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a"
dependencies:
map-or-similar "^1.5.0"
memory-fs@^0.4.0, memory-fs@~0.4.1: memory-fs@^0.4.0, memory-fs@~0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
@ -5978,7 +5976,7 @@ promise-inflight@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: prop-types@^15.6.1, prop-types@^15.6.2:
version "15.7.2" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
dependencies: dependencies:
@ -6148,7 +6146,7 @@ react-hot-loader@^4.9.0:
shallowequal "^1.0.2" shallowequal "^1.0.2"
source-map "^0.7.3" source-map "^0.7.3"
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.8.6" version "16.8.6"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
@ -6156,17 +6154,6 @@ react-lifecycles-compat@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
react-redux@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2"
dependencies:
"@babel/runtime" "^7.4.5"
hoist-non-react-statics "^3.3.0"
invariant "^2.2.4"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.8.6"
react-router-dom@^5.0.1: react-router-dom@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.1.tgz#ee66f4a5d18b6089c361958e443489d6bab714be" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.1.tgz#ee66f4a5d18b6089c361958e443489d6bab714be"
@ -6291,11 +6278,11 @@ reduce-css-calc@^2.0.0:
css-unit-converter "^1.1.1" css-unit-converter "^1.1.1"
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
redux-persist@*, redux-persist@^5.10.0: redux-thunk@^2.3.0:
version "5.10.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-5.10.0.tgz#5d8d802c5571e55924efc1c3a9b23575283be62b" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
redux@^4.0.0, redux@^4.0.1: redux@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5"
dependencies: dependencies:
@ -6734,7 +6721,7 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1" inherits "^2.0.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
shallowequal@^1.0.2: shallowequal@^1.0.2, shallowequal@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
@ -7378,10 +7365,18 @@ type-is@~1.6.16:
media-typer "0.3.0" media-typer "0.3.0"
mime-types "~2.1.18" mime-types "~2.1.18"
type-zoo@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/type-zoo/-/type-zoo-3.3.0.tgz#d5b59393b09a48ac30380c50e2369e828817e9b3"
typedarray@^0.0.6: typedarray@^0.0.6:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
typelevel-ts@^0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/typelevel-ts/-/typelevel-ts-0.3.5.tgz#9f242c193fa6c5489d2342c642ab8912a065af9d"
typescript@^3.3.1: typescript@^3.3.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.1.tgz#6de14e1db4b8a006ac535e482c8ba018c55f750b" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.1.tgz#6de14e1db4b8a006ac535e482c8ba018c55f750b"
@ -7537,6 +7532,16 @@ url@^0.11.0:
punycode "1.3.2" punycode "1.3.2"
querystring "0.2.0" querystring "0.2.0"
use-force-update@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/use-force-update/-/use-force-update-1.0.5.tgz#9b7192f73f6cb90d592b225858c1562719d7c184"
use-react-router@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/use-react-router/-/use-react-router-1.0.7.tgz#04216066d87e45040309f24d2fd5e9f28308b3e2"
dependencies:
use-force-update "^1.0.5"
use@^3.1.0: use@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544" resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544"