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

View File

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

View File

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

View File

@ -1,111 +1,96 @@
import * as React from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import login from '@/api/auth/login';
import { httpErrorToHuman } from '@/api/http';
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
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<{
errorMessage?: string;
isLoading: boolean;
username?: string;
password?: string;
}>;
export default () => {
const [ username, setUsername ] = useState('');
const [ password, setPassword ] = useState('');
const [ isLoading, setLoading ] = useState(false);
const { history } = useRouter();
export default class LoginContainer extends React.PureComponent<RouteComponentProps, State> {
state: State = {
isLoading: false,
};
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
submit = (e: React.FormEvent<HTMLFormElement>) => {
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const { username, password } = this.state;
setLoading(true);
clearFlashes();
this.setState({ isLoading: true }, () => {
login(username!, password!)
.then(response => {
if (response.complete) {
// @ts-ignore
window.location = response.intended || '/';
return;
}
login(username!, password!)
.then(response => {
if (response.complete) {
// @ts-ignore
window.location = response.intended || '/';
return;
}
this.props.history.replace('/login/checkpoint', {
token: response.confirmationToken,
});
})
.catch(error => this.setState({
isLoading: false,
errorMessage: httpErrorToHuman(error),
}, () => console.error(error)));
});
history.replace('/login/checkpoint', { token: response.confirmationToken });
})
.catch(error => {
console.error(error);
setLoading(false);
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
});
};
canSubmit () {
if (!this.state.username || !this.state.password) {
return false;
}
const canSubmit = () => username && password && username.length > 0 && password.length > 0;
return this.state.username.length > 0 && this.state.password.length > 0;
}
// @ts-ignore
handleFieldUpdate = (e: React.ChangeEvent<HTMLInputElement>) => this.setState({
[e.target.id]: e.target.value,
});
render () {
return (
<React.Fragment>
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
Login to Continue
</h2>
<NetworkErrorMessage message={this.state.errorMessage}/>
<LoginFormContainer onSubmit={this.submit}>
<label htmlFor={'username'}>Username or Email</label>
return (
<React.Fragment>
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
Login to Continue
</h2>
<FlashMessageRender/>
<LoginFormContainer onSubmit={submit}>
<label htmlFor={'username'}>Username or Email</label>
<input
id={'username'}
autoFocus={true}
required={true}
className={'input'}
onChange={e => setUsername(e.target.value)}
disabled={isLoading}
/>
<div className={'mt-6'}>
<label htmlFor={'password'}>Password</label>
<input
id={'username'}
autoFocus={true}
id={'password'}
required={true}
type={'password'}
className={'input'}
onChange={this.handleFieldUpdate}
disabled={this.state.isLoading}
onChange={e => setPassword(e.target.value)}
disabled={isLoading}
/>
<div className={'mt-6'}>
<label htmlFor={'password'}>Password</label>
<input
id={'password'}
required={true}
type={'password'}
className={'input'}
onChange={this.handleFieldUpdate}
disabled={this.state.isLoading}
/>
</div>
<div className={'mt-6'}>
<button
type={'submit'}
className={'btn btn-primary btn-jumbo'}
disabled={this.state.isLoading || !this.canSubmit()}
>
{this.state.isLoading ?
<span className={'spinner white'}>&nbsp;</span>
:
'Login'
}
</button>
</div>
<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>
);
}
}
</div>
<div className={'mt-6'}>
<button
type={'submit'}
className={'btn btn-primary btn-jumbo'}
disabled={isLoading || !canSubmit()}
>
{isLoading ?
<span className={'spinner white'}>&nbsp;</span>
:
'Login'
}
</button>
</div>
<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,
}}
>
<div className={'flex-none'}>
<div className={'flex-none select-none'}>
<img src={'/assets/pterodactyl.svg'} className={'w-64'}/>
</div>
<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 { parse } from 'query-string';
import { Link } from 'react-router-dom';
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
import performPasswordReset from '@/api/auth/performPasswordReset';
import { httpErrorToHuman } from '@/api/http';
import { connect } from 'react-redux';
import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash';
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<{
email?: string;
password?: string;
passwordConfirm?: string;
isLoading: boolean;
errorMessage?: string;
}>;
type Props = Readonly<RouteComponentProps<{ token: string }> & {}>;
type Props = Readonly<RouteComponentProps<{ token: string }> & {
pushFlashMessage: typeof pushFlashMessage;
clearAllFlashMessages: typeof clearAllFlashMessages;
}>;
export default (props: Props) => {
const [ isLoading, setIsLoading ] = useState(false);
const [ email, setEmail ] = useState('');
const [ password, setPassword ] = useState('');
const [ passwordConfirm, setPasswordConfirm ] = useState('');
class ResetPasswordContainer extends React.PureComponent<Props, State> {
state: State = {
isLoading: false,
};
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationState>) => actions.flashes);
componentDidMount () {
const parsed = parse(this.props.location.search);
this.setState({ email: parsed.email as string || undefined });
const parsed = parse(props.location.search);
if (email.length === 0 && parsed.email) {
setEmail(parsed.email as string);
}
canSubmit () {
if (!this.state.password || !this.state.email) {
return false;
}
const canSubmit = () => password && email && password.length >= 8 && password === passwordConfirm;
return this.state.password.length >= 8 && this.state.password === this.state.passwordConfirm;
}
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>) => {
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const { password, passwordConfirm, email } = this.state;
if (!password || !email || !passwordConfirm) {
return;
}
this.props.clearAllFlashMessages();
this.setState({ isLoading: true }, () => {
performPasswordReset(email, {
token: this.props.match.params.token,
password: password,
passwordConfirmation: passwordConfirm,
})
.then(response => {
if (response.redirectTo) {
// @ts-ignore
window.location = response.redirectTo;
return;
}
setIsLoading(true);
clearFlashes();
this.props.pushFlashMessage({
type: 'success',
message: 'Your password has been reset, please login to continue.',
});
this.props.history.push('/login');
})
.catch(error => {
console.error(error);
this.setState({ errorMessage: httpErrorToHuman(error) });
})
.then(() => this.setState({ isLoading: false }));
});
performPasswordReset(email, {
token: props.match.params.token, password, passwordConfirmation: passwordConfirm,
})
.then(() => {
addFlash({ type: 'success', message: 'Your password has been reset, please login to continue.' });
props.history.push('/login');
})
.catch(error => {
console.error(error);
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
})
.then(() => setIsLoading(false));
};
render () {
return (
<div>
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
Reset Password
</h2>
<NetworkErrorMessage message={this.state.errorMessage}/>
<LoginFormContainer onSubmit={this.onSubmit}>
<label>Email</label>
<input value={this.state.email || ''} disabled={true}/>
<div className={'mt-6'}>
<label htmlFor={'new_password'}>New Password</label>
<input
id={'new_password'}
type={'password'}
required={true}
onChange={this.onPasswordChange}
/>
<p className={'input-help'}>
Passwords must be at least 8 characters in length.
</p>
</div>
<div className={'mt-6'}>
<label htmlFor={'new_password_confirm'}>Confirm New Password</label>
<input
id={'new_password_confirm'}
type={'password'}
required={true}
onChange={this.onPasswordConfirmChange}
/>
</div>
<div className={'mt-6'}>
<button
type={'submit'}
className={'btn btn-primary btn-jumbo'}
disabled={this.state.isLoading || !this.canSubmit()}
>
{this.state.isLoading ?
<span className={'spinner white'}>&nbsp;</span>
:
'Reset Password'
}
</button>
</div>
<div className={'mt-6 text-center'}>
<Link
to={'/login'}
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
>
Return to Login
</Link>
</div>
</LoginFormContainer>
</div>
);
}
}
const mapDispatchToProps = {
pushFlashMessage,
clearAllFlashMessages,
return (
<div>
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
Reset Password
</h2>
<FlashMessageRender/>
<LoginFormContainer onSubmit={submit}>
<label>Email</label>
<input className={'input'} value={email} disabled={true}/>
<div className={'mt-6'}>
<label htmlFor={'new_password'}>New Password</label>
<input
id={'new_password'}
className={'input'}
type={'password'}
required={true}
onChange={e => setPassword(e.target.value)}
/>
<p className={'input-help'}>
Passwords must be at least 8 characters in length.
</p>
</div>
<div className={'mt-6'}>
<label htmlFor={'new_password_confirm'}>Confirm New Password</label>
<input
id={'new_password_confirm'}
className={'input'}
type={'password'}
required={true}
onChange={e => setPasswordConfirm(e.target.value)}
/>
</div>
<div className={'mt-6'}>
<button
type={'submit'}
className={'btn btn-primary btn-jumbo'}
disabled={isLoading || !canSubmit()}
>
{isLoading ?
<span className={'spinner white'}>&nbsp;</span>
:
'Reset Password'
}
</button>
</div>
<div className={'mt-6 text-center'}>
<Link
to={'/login'}
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
>
Return to Login
</Link>
</div>
</LoginFormContainer>
</div>
);
};
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 { BrowserRouter, Route, Switch } from 'react-router-dom';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import FlashMessageRender from '@/components/FlashMessageRender';
import DesignElements from '@/components/account/DesignElements';
export default class AccountRouter extends React.PureComponent {
@ -13,7 +12,6 @@ export default class AccountRouter extends React.PureComponent {
<TransitionGroup className={'route-transition-group'}>
<CSSTransition key={location.key} timeout={150} classNames={'fade'}>
<section>
<FlashMessageRender/>
<Switch location={location}>
<Route path={'/'} 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 { CSSTransition, TransitionGroup } from 'react-transition-group';
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
import FlashMessageRender from '@/components/FlashMessageRender';
import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
@ -16,7 +15,6 @@ export default class AuthenticationRouter extends React.PureComponent {
<TransitionGroup className={'route-transition-group mt-32'}>
<CSSTransition key={location.key} timeout={150} classNames={'fade'}>
<section>
<FlashMessageRender/>
<Switch location={location}>
<Route path={'/login'} component={LoginContainer} exact/>
<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,
"noImplicitReturns": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"lib": ["es2015", "dom"],

View File

@ -4,7 +4,6 @@ const tailwind = require('tailwindcss');
const glob = require('glob-all');
const AssetsManifestPlugin = require('webpack-assets-manifest');
const CleanPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PurgeCssPlugin = require('purgecss-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';
let plugins = [
new CleanPlugin(path.resolve(__dirname, 'public/assets')),
new MiniCssExtractPlugin({ filename: isProduction ? 'bundle.[chunkhash:8].css' : 'bundle.[hash:8].css' }),
new AssetsManifestPlugin({
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-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"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
dependencies:
@ -751,13 +751,6 @@
version "4.7.2"
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":
version "4.14.119"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39"
@ -778,15 +771,6 @@
dependencies:
"@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":
version "4.3.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.3.tgz#7837e3e9fefbc84a8f6c8a51dca004f4e83e94e3"
@ -815,12 +799,6 @@
"@types/prop-types" "*"
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":
version "1.13.6"
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:
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:
version "2.1.0"
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"
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:
version "1.1.1"
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"
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:
version "2.1.0"
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"
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"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies:
@ -4535,6 +4523,10 @@ map-cache@^0.2.2:
version "0.2.2"
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:
version "1.0.0"
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"
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:
version "0.4.1"
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"
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"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
dependencies:
@ -6148,7 +6146,7 @@ react-hot-loader@^4.9.0:
shallowequal "^1.0.2"
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"
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"
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:
version "5.0.1"
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"
postcss-value-parser "^3.3.0"
redux-persist@*, redux-persist@^5.10.0:
version "5.10.0"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-5.10.0.tgz#5d8d802c5571e55924efc1c3a9b23575283be62b"
redux-thunk@^2.3.0:
version "2.3.0"
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"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5"
dependencies:
@ -6734,7 +6721,7 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shallowequal@^1.0.2:
shallowequal@^1.0.2, shallowequal@^1.1.0:
version "1.1.0"
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"
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:
version "0.0.6"
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:
version "3.3.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.1.tgz#6de14e1db4b8a006ac535e482c8ba018c55f750b"
@ -7537,6 +7532,16 @@ url@^0.11.0:
punycode "1.3.2"
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:
version "3.1.0"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544"