mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Reset password page (#181)
* Extracts stacked boxes UI to a re-usable component * Presence validator * Equality validator * Adds ResetPasswordFrom * PasswordResetPage component and route * Ex icon on forgot pw page goes to login * Smooth out the fonts so they match the mocks * Remove dynamic background and refactor colors
This commit is contained in:
parent
cfdd665673
commit
482d025d05
30 changed files with 599 additions and 287 deletions
BIN
assets/images/background.png
Normal file
BIN
assets/images/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 353 KiB |
|
|
@ -27,3 +27,8 @@
|
|||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const { color } = styles;
|
|||
export default {
|
||||
footerStyles: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: color.darkGrey,
|
||||
backgroundColor: color.footerBg,
|
||||
height: '55px',
|
||||
textAlign: 'center',
|
||||
position: 'absolute',
|
||||
|
|
|
|||
|
|
@ -1,23 +1,11 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import componentStyles from './styles';
|
||||
import { loadBackground, removeBackground, resizeBackground } from '../../utilities/backgroundImage';
|
||||
|
||||
export class LoginRoutes extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.element,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { window } = global;
|
||||
|
||||
loadBackground();
|
||||
window.onresize = resizeBackground;
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
removeBackground();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { containerStyles } = componentStyles;
|
||||
const { children } = this.props;
|
||||
|
|
@ -32,4 +20,3 @@ export class LoginRoutes extends Component {
|
|||
}
|
||||
|
||||
export default LoginRoutes;
|
||||
|
||||
|
|
|
|||
69
frontend/components/StackedWhiteBoxes/StackedWhiteBoxes.jsx
Normal file
69
frontend/components/StackedWhiteBoxes/StackedWhiteBoxes.jsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import radium from 'radium';
|
||||
import { Link } from 'react-router';
|
||||
import componentStyles from './styles';
|
||||
|
||||
class StackedWhiteBoxes extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.element,
|
||||
headerText: PropTypes.string,
|
||||
leadText: PropTypes.string,
|
||||
previousLocation: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
style: {},
|
||||
};
|
||||
|
||||
renderBackButton = () => {
|
||||
const { previousLocation } = this.props;
|
||||
const { exStyles, exWrapperStyles } = componentStyles;
|
||||
|
||||
if (!previousLocation) return false;
|
||||
|
||||
return (
|
||||
<div style={exWrapperStyles}>
|
||||
<Link style={exStyles} to={previousLocation}>x</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeader = () => {
|
||||
const { headerStyles, headerWrapperStyles } = componentStyles;
|
||||
const { headerText, style } = this.props;
|
||||
|
||||
return (
|
||||
<div style={[headerWrapperStyles, style.headerWrapper]}>
|
||||
<p style={headerStyles}>{headerText}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, leadText } = this.props;
|
||||
const {
|
||||
boxStyles,
|
||||
containerStyles,
|
||||
smallTabStyles,
|
||||
tabStyles,
|
||||
textStyles,
|
||||
} = componentStyles;
|
||||
const { renderBackButton, renderHeader } = this;
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<div style={smallTabStyles} />
|
||||
<div style={tabStyles} />
|
||||
<div style={boxStyles}>
|
||||
{renderBackButton()}
|
||||
{renderHeader()}
|
||||
<p style={textStyles}>{leadText}</p>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default radium(StackedWhiteBoxes);
|
||||
1
frontend/components/StackedWhiteBoxes/index.js
Normal file
1
frontend/components/StackedWhiteBoxes/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default from './StackedWhiteBoxes';
|
||||
66
frontend/components/StackedWhiteBoxes/styles.js
Normal file
66
frontend/components/StackedWhiteBoxes/styles.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import styles from '../../styles';
|
||||
|
||||
const { border, color, font, padding } = styles;
|
||||
|
||||
export default {
|
||||
boxStyles: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: color.white,
|
||||
borderTopLeftRadius: border.radius.base,
|
||||
borderTopRightRadius: border.radius.base,
|
||||
boxShadow: border.shadow.blur,
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: padding.base,
|
||||
width: '522px',
|
||||
},
|
||||
containerStyles: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
exStyles: {
|
||||
color: color.lightGrey,
|
||||
textDecoration: 'none',
|
||||
},
|
||||
exWrapperStyles: {
|
||||
textAlign: 'right',
|
||||
width: '100%',
|
||||
},
|
||||
headerStyles: {
|
||||
fontFamily: "'Oxygen', sans-serif",
|
||||
fontSize: font.large,
|
||||
fontWeight: '300',
|
||||
color: color.textUltradark,
|
||||
lineHeight: '32px',
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
headerWrapperStyles: {
|
||||
width: '100%',
|
||||
},
|
||||
tabStyles: {
|
||||
backgroundColor: color.white,
|
||||
borderTopLeftRadius: border.radius.base,
|
||||
borderTopRightRadius: border.radius.base,
|
||||
boxShadow: border.shadow.blur,
|
||||
height: '20px',
|
||||
width: '460px',
|
||||
},
|
||||
textStyles: {
|
||||
color: color.purpleGrey,
|
||||
fontSize: font.medium,
|
||||
},
|
||||
smallTabStyles: {
|
||||
backgroundColor: color.white,
|
||||
borderTopLeftRadius: border.radius.base,
|
||||
borderTopRightRadius: border.radius.base,
|
||||
boxShadow: border.shadow.blur,
|
||||
height: '20px',
|
||||
marginTop: padding.base,
|
||||
width: '400px',
|
||||
},
|
||||
};
|
||||
|
|
@ -19,7 +19,7 @@ export default {
|
|||
forgotPasswordStyles: {
|
||||
fontSize: font.medium,
|
||||
textDecoration: 'none',
|
||||
color: color.lightGrey,
|
||||
color: color.accentText,
|
||||
},
|
||||
forgotPasswordWrapperStyles: {
|
||||
marginTop: padding.base,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import componentStyles from './styles';
|
||||
import GradientButton from '../../buttons/GradientButton';
|
||||
import InputFieldWithIcon from '../fields/InputFieldWithIcon';
|
||||
import validatePresence from '../validators/validate_presence';
|
||||
import validateEquality from '../validators/validate_equality';
|
||||
|
||||
class ResetPasswordForm extends Component {
|
||||
static propTypes = {
|
||||
onSubmit: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errors: {
|
||||
new_password: null,
|
||||
new_password_confirmation: null,
|
||||
},
|
||||
formData: {
|
||||
new_password: null,
|
||||
new_password_confirmation: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onFormSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { validate } = this;
|
||||
const { onSubmit } = this.props;
|
||||
const { formData } = this.state;
|
||||
|
||||
if (validate()) {
|
||||
return onSubmit(formData);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onInputChange = (inputName) => {
|
||||
return ({ target }) => {
|
||||
const { formData } = this.state;
|
||||
const { value } = target;
|
||||
|
||||
this.setState({
|
||||
errors: {
|
||||
new_password: null,
|
||||
new_password_confirmation: null,
|
||||
},
|
||||
formData: {
|
||||
...formData,
|
||||
[inputName]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
validate = () => {
|
||||
const {
|
||||
errors,
|
||||
formData: {
|
||||
new_password: newPassword,
|
||||
new_password_confirmation: newPasswordConfirmation,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
if (!validatePresence(newPassword)) {
|
||||
this.setState({
|
||||
errors: {
|
||||
...errors,
|
||||
new_password: 'New Password field must be completed',
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validatePresence(newPasswordConfirmation)) {
|
||||
this.setState({
|
||||
errors: {
|
||||
...errors,
|
||||
new_password_confirmation: 'New Password Confirmation field must be completed',
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateEquality(newPassword, newPasswordConfirmation)) {
|
||||
this.setState({
|
||||
errors: {
|
||||
...errors,
|
||||
new_password_confirmation: 'Passwords Do Not Match',
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { errors } = this.state;
|
||||
const {
|
||||
formStyles,
|
||||
inputStyles,
|
||||
submitButtonStyles,
|
||||
} = componentStyles;
|
||||
const { onFormSubmit, onInputChange } = this;
|
||||
|
||||
return (
|
||||
<form onSubmit={onFormSubmit} style={formStyles}>
|
||||
<InputFieldWithIcon
|
||||
error={errors.new_password}
|
||||
iconName="lock"
|
||||
name="new_password"
|
||||
onChange={onInputChange('new_password')}
|
||||
placeholder="New Password"
|
||||
style={inputStyles}
|
||||
type="password"
|
||||
/>
|
||||
<InputFieldWithIcon
|
||||
error={errors.new_password_confirmation}
|
||||
iconName="lock"
|
||||
name="new_password_confirmation"
|
||||
onChange={onInputChange('new_password_confirmation')}
|
||||
placeholder="Confirm Password"
|
||||
style={inputStyles}
|
||||
type="password"
|
||||
/>
|
||||
<GradientButton
|
||||
onClick={onFormSubmit}
|
||||
style={submitButtonStyles}
|
||||
text="Reset Password"
|
||||
type="submit"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ResetPasswordForm;
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import React from 'react';
|
||||
import expect, { createSpy, restoreSpies } from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
import { noop } from 'lodash';
|
||||
import ResetPasswordForm from './ResetPasswordForm';
|
||||
import { fillInFormInput } from '../../../test/helpers';
|
||||
|
||||
describe('ResetPasswordForm - component', () => {
|
||||
const newPassword = 'my new password';
|
||||
|
||||
afterEach(restoreSpies);
|
||||
|
||||
it('updates component state when the new_password field is changed', () => {
|
||||
const form = mount(<ResetPasswordForm onSubmit={noop} />);
|
||||
|
||||
const newPasswordField = form.find({ name: 'new_password' });
|
||||
fillInFormInput(newPasswordField, newPassword);
|
||||
|
||||
const { formData } = form.state();
|
||||
expect(formData).toContain({ new_password: newPassword });
|
||||
});
|
||||
|
||||
it('updates component state when the new_password_confirmation field is changed', () => {
|
||||
const form = mount(<ResetPasswordForm onSubmit={noop} />);
|
||||
|
||||
const newPasswordField = form.find({ name: 'new_password_confirmation' });
|
||||
fillInFormInput(newPasswordField, newPassword);
|
||||
|
||||
const { formData } = form.state();
|
||||
expect(formData).toContain({ new_password_confirmation: newPassword });
|
||||
});
|
||||
|
||||
it('it does not submit the form when the form fields have not been filled out', () => {
|
||||
const submitSpy = createSpy();
|
||||
const form = mount(<ResetPasswordForm onSubmit={submitSpy} />);
|
||||
const submitBtn = form.find('button');
|
||||
|
||||
submitBtn.simulate('submit');
|
||||
|
||||
const { errors } = form.state();
|
||||
expect(errors.new_password).toEqual('New Password field must be completed');
|
||||
expect(submitSpy).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('it does not submit the form when only the new password field has been filled out', () => {
|
||||
const submitSpy = createSpy();
|
||||
const form = mount(<ResetPasswordForm onSubmit={submitSpy} />);
|
||||
const newPasswordField = form.find({ name: 'new_password' });
|
||||
fillInFormInput(newPasswordField, newPassword);
|
||||
const submitBtn = form.find('button');
|
||||
|
||||
submitBtn.simulate('submit');
|
||||
|
||||
const { errors } = form.state();
|
||||
expect(errors.new_password_confirmation).toEqual('New Password Confirmation field must be completed');
|
||||
expect(submitSpy).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submits the form data when the form is submitted', () => {
|
||||
const submitSpy = createSpy();
|
||||
const form = mount(<ResetPasswordForm onSubmit={submitSpy} />);
|
||||
const newPasswordField = form.find({ name: 'new_password' });
|
||||
const newPasswordConfirmationField = form.find({ name: 'new_password_confirmation' });
|
||||
const submitBtn = form.find('button');
|
||||
|
||||
fillInFormInput(newPasswordField, newPassword);
|
||||
fillInFormInput(newPasswordConfirmationField, newPassword);
|
||||
submitBtn.simulate('submit');
|
||||
|
||||
expect(submitSpy).toHaveBeenCalledWith({
|
||||
new_password: newPassword,
|
||||
new_password_confirmation: newPassword,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not submit the form if the new password confirmation does not match', () => {
|
||||
const submitSpy = createSpy();
|
||||
const form = mount(<ResetPasswordForm onSubmit={submitSpy} />);
|
||||
const newPasswordField = form.find({ name: 'new_password' });
|
||||
const newPasswordConfirmationField = form.find({ name: 'new_password_confirmation' });
|
||||
const submitBtn = form.find('button');
|
||||
|
||||
fillInFormInput(newPasswordField, newPassword);
|
||||
fillInFormInput(newPasswordConfirmationField, 'not my new password');
|
||||
submitBtn.simulate('submit');
|
||||
|
||||
expect(submitSpy).toNotHaveBeenCalled();
|
||||
expect(form.state().errors).toInclude({
|
||||
new_password_confirmation: 'Passwords Do Not Match',
|
||||
});
|
||||
});
|
||||
});
|
||||
1
frontend/components/forms/ResetPasswordForm/index.js
Normal file
1
frontend/components/forms/ResetPasswordForm/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default from './ResetPasswordForm';
|
||||
18
frontend/components/forms/ResetPasswordForm/styles.js
Normal file
18
frontend/components/forms/ResetPasswordForm/styles.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import styles from '../../../styles';
|
||||
|
||||
const { border, padding } = styles;
|
||||
|
||||
export default {
|
||||
formStyles: {
|
||||
width: '100%',
|
||||
},
|
||||
inputStyles: {
|
||||
width: '100%',
|
||||
},
|
||||
submitButtonStyles: {
|
||||
borderTopLeftRadius: border.radius.base,
|
||||
borderTopRightRadius: border.radius.base,
|
||||
marginTop: padding.base,
|
||||
padding: padding.medium,
|
||||
},
|
||||
};
|
||||
|
|
@ -8,7 +8,7 @@ export default {
|
|||
position: 'relative',
|
||||
},
|
||||
errorStyles: {
|
||||
color: color.red,
|
||||
color: color.alert,
|
||||
fontSize: font.small,
|
||||
textTransform: 'lowercase',
|
||||
},
|
||||
|
|
@ -20,7 +20,7 @@ export default {
|
|||
inputErrorStyles: (error) => {
|
||||
if (error) {
|
||||
return {
|
||||
borderBottomColor: color.red,
|
||||
borderBottomColor: color.alert,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -33,8 +33,8 @@ export default {
|
|||
borderTop: 'none',
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomStyle: 'solid',
|
||||
borderBottomColor: color.brightPurple,
|
||||
color: '#A2A1C8',
|
||||
borderBottomColor: color.brand,
|
||||
color: color.accentText,
|
||||
width: '378px',
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
|
|
@ -44,7 +44,7 @@ export default {
|
|||
if (value) {
|
||||
return {
|
||||
...baseStyles,
|
||||
color: color.grey,
|
||||
color: color.textUltradark,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ export default {
|
|||
if (!value) return { visibility: 'hidden', height: '22px' };
|
||||
|
||||
return {
|
||||
color: color.brightPurple,
|
||||
color: color.brand,
|
||||
fontSize: font.small,
|
||||
textTransform: 'lowercase',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { isEqual } from 'lodash';
|
||||
|
||||
export default (actual, expected) => {
|
||||
return isEqual(actual, expected);
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import expect from 'expect';
|
||||
import validateEquality from './index';
|
||||
|
||||
describe('validateEquality - validator', () => {
|
||||
it('returns true for equal inputs', () => {
|
||||
expect(validateEquality('thegnarco', 'thegnarco')).toEqual(true);
|
||||
expect(validateEquality(1, 1)).toEqual(true);
|
||||
expect(validateEquality(1.0, 1)).toEqual(true);
|
||||
expect(validateEquality(['thegnarco'], ['thegnarco'])).toEqual(true);
|
||||
expect(validateEquality({ hello: 'world' }, { hello: 'world' })).toEqual(true);
|
||||
expect(validateEquality({ foo: { bar: 'baz' } }, { foo: { bar: 'baz' } })).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false for unequal inputs', () => {
|
||||
expect(validateEquality('thegnarco', 'thegnar')).toEqual(false);
|
||||
expect(validateEquality(1, 'thegnar')).toEqual(false);
|
||||
expect(validateEquality(['thegnarco'], [1])).toEqual(false);
|
||||
expect(validateEquality({ hello: 'world' }, { hello: 'foo' })).toEqual(false);
|
||||
expect(validateEquality({ foo: { bar: 'baz' } }, { foo: { bar: 'foo' } })).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default (actual) => {
|
||||
return !!actual;
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import expect from 'expect';
|
||||
import validatePresence from './index';
|
||||
|
||||
const validInputs = [
|
||||
[1, 2, 3],
|
||||
{ hello: 'world' },
|
||||
'hi@thegnar.co',
|
||||
];
|
||||
|
||||
const invalidInputs = [
|
||||
'',
|
||||
undefined,
|
||||
false,
|
||||
null,
|
||||
];
|
||||
|
||||
describe('validatePresence - validator', () => {
|
||||
it('returns true for valid inputs', () => {
|
||||
validInputs.forEach(input => {
|
||||
expect(validatePresence(input)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false for invalid inputs', () => {
|
||||
invalidInputs.forEach(input => {
|
||||
expect(validatePresence(input)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from '../../redux/nodes/components/ForgotPasswordPage/actions';
|
||||
import ForgotPasswordForm from '../../components/forms/ForgotPasswordForm';
|
||||
import Icon from '../../components/icons/Icon';
|
||||
import StackedWhiteBoxes from '../../components/StackedWhiteBoxes';
|
||||
|
||||
export class ForgotPasswordPage extends Component {
|
||||
static propTypes = {
|
||||
|
|
@ -71,25 +72,20 @@ export class ForgotPasswordPage extends Component {
|
|||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
containerStyles,
|
||||
forgotPasswordStyles,
|
||||
headerStyles,
|
||||
smallWhiteTabStyles,
|
||||
textStyles,
|
||||
whiteTabStyles,
|
||||
} = componentStyles;
|
||||
const leadText = 'If you’ve forgotten your password enter your email below and we will email you a link so that you can reset your password.';
|
||||
const whiteBoxOverrideStyles = {
|
||||
headerWrapper: { textAlign: 'left' },
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<div style={smallWhiteTabStyles} />
|
||||
<div style={whiteTabStyles} />
|
||||
<div style={forgotPasswordStyles}>
|
||||
<p style={headerStyles}>Forgot Password</p>
|
||||
<p style={textStyles}>If you’ve forgotten your password enter your email below and we will email you a link so that you can reset your password.</p>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
<StackedWhiteBoxes
|
||||
headerText="Forgot Password"
|
||||
leadText={leadText}
|
||||
previousLocation="/login"
|
||||
style={whiteBoxOverrideStyles}
|
||||
>
|
||||
{this.renderContent()}
|
||||
</StackedWhiteBoxes>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,6 @@ import styles from '../../styles';
|
|||
const { border, color, font, padding } = styles;
|
||||
|
||||
export default {
|
||||
containerStyles: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
emailSentButtonWrapperStyles: {
|
||||
backgroundColor: color.successLight,
|
||||
borderRadius: border.radius.base,
|
||||
|
|
@ -37,45 +31,4 @@ export default {
|
|||
emailTextStyles: {
|
||||
color: color.link,
|
||||
},
|
||||
forgotPasswordStyles: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: color.white,
|
||||
borderTopLeftRadius: border.radius.base,
|
||||
borderTopRightRadius: border.radius.base,
|
||||
boxShadow: border.shadow.blur,
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: padding.base,
|
||||
width: '522px',
|
||||
},
|
||||
headerStyles: {
|
||||
fontFamily: "'Oxygen', sans-serif",
|
||||
fontSize: font.large,
|
||||
fontWeight: '300',
|
||||
color: color.mediumGrey,
|
||||
lineHeight: '32px',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
smallWhiteTabStyles: {
|
||||
backgroundColor: color.white,
|
||||
borderTopLeftRadius: border.radius.base,
|
||||
borderTopRightRadius: border.radius.base,
|
||||
boxShadow: border.shadow.blur,
|
||||
height: '20px',
|
||||
marginTop: padding.base,
|
||||
width: '400px',
|
||||
},
|
||||
textStyles: {
|
||||
color: color.purpleGrey,
|
||||
fontSize: font.medium,
|
||||
},
|
||||
whiteTabStyles: {
|
||||
backgroundColor: color.white,
|
||||
borderTopLeftRadius: border.radius.base,
|
||||
borderTopRightRadius: border.radius.base,
|
||||
boxShadow: border.shadow.blur,
|
||||
height: '20px',
|
||||
width: '460px',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { Component, PropTypes } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { push } from 'react-router-redux';
|
||||
import componentStyles from './styles';
|
||||
import { loadBackground, resizeBackground } from '../../utilities/backgroundImage';
|
||||
import local from '../../utilities/local';
|
||||
import LoginForm from '../../components/forms/LoginForm';
|
||||
import { loginUser } from '../../redux/nodes/auth/actions';
|
||||
|
|
@ -17,15 +16,11 @@ export class LoginPage extends Component {
|
|||
|
||||
componentWillMount () {
|
||||
const { dispatch } = this.props;
|
||||
const { window } = global;
|
||||
|
||||
if (local.getItem('auth_token')) {
|
||||
return dispatch(push('/'));
|
||||
}
|
||||
|
||||
loadBackground();
|
||||
window.onresize = resizeBackground;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,10 @@
|
|||
import expect, { spyOn, restoreSpies } from 'expect';
|
||||
import expect from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
import { noop } from 'lodash';
|
||||
import * as bgImageUtility from '../../utilities/backgroundImage';
|
||||
import { connectedComponent, reduxMockStore } from '../../test/helpers';
|
||||
import local from '../../utilities/local';
|
||||
import LoginPage from './LoginPage';
|
||||
|
||||
describe('LoginPage - component', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(bgImageUtility, 'loadBackground').andReturn(noop);
|
||||
spyOn(bgImageUtility, 'resizeBackground').andReturn(noop);
|
||||
});
|
||||
|
||||
afterEach(restoreSpies);
|
||||
|
||||
context('when the user is not logged in', () => {
|
||||
const mockStore = reduxMockStore({ auth: {} });
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const { color, font, padding } = styles;
|
|||
|
||||
export default {
|
||||
loginSuccessStyles: {
|
||||
color: color.green,
|
||||
color: color.success,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: font.large,
|
||||
letterSpacing: '2px',
|
||||
|
|
@ -12,7 +12,7 @@ export default {
|
|||
},
|
||||
subtextStyles: {
|
||||
fontSize: font.medium,
|
||||
color: color.lightGrey,
|
||||
color: color.accentText,
|
||||
},
|
||||
whiteBoxStyles: {
|
||||
backgroundColor: color.white,
|
||||
|
|
|
|||
54
frontend/pages/ResetPasswordPage/ResetPasswordPage.jsx
Normal file
54
frontend/pages/ResetPasswordPage/ResetPasswordPage.jsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { noop } from 'lodash';
|
||||
import { push } from 'react-router-redux';
|
||||
import ResetPasswordForm from '../../components/forms/ResetPasswordForm';
|
||||
import StackedWhiteBoxes from '../../components/StackedWhiteBoxes';
|
||||
|
||||
export class ResetPasswordPage extends Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
token: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
dispatch: noop,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { dispatch, token } = this.props;
|
||||
|
||||
if (!token) return dispatch(push('/login'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onSubmit = (formData) => {
|
||||
console.log('ResetPasswordForm data', formData);
|
||||
return false;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { onSubmit } = this;
|
||||
|
||||
return (
|
||||
<StackedWhiteBoxes
|
||||
headerText="Reset Password"
|
||||
leadText="Create a new password using at least one letter, one numeral and seven characters."
|
||||
>
|
||||
<ResetPasswordForm onSubmit={onSubmit} />
|
||||
</StackedWhiteBoxes>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { query = {} } = ownProps.location || {};
|
||||
const { token } = query;
|
||||
|
||||
return {
|
||||
token,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(ResetPasswordPage);
|
||||
32
frontend/pages/ResetPasswordPage/ResetPasswordPage.tests.jsx
Normal file
32
frontend/pages/ResetPasswordPage/ResetPasswordPage.tests.jsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import expect from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
import ConnectedPage, { ResetPasswordPage } from './ResetPasswordPage';
|
||||
import testHelpers from '../../test/helpers';
|
||||
|
||||
describe('ResetPasswordPage - component', () => {
|
||||
it('renders a ResetPasswordForm', () => {
|
||||
const page = mount(<ResetPasswordPage token="ABC123" />);
|
||||
|
||||
expect(page.find('ResetPasswordForm').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('Redirects to the login page when there is no token', () => {
|
||||
const redirectToLoginAction = {
|
||||
type: '@@router/CALL_HISTORY_METHOD',
|
||||
payload: {
|
||||
method: 'push',
|
||||
args: ['/login'],
|
||||
},
|
||||
};
|
||||
const { reduxMockStore, connectedComponent } = testHelpers;
|
||||
const mockStore = reduxMockStore();
|
||||
|
||||
mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
expect(dispatchedActions).toInclude(redirectToLoginAction);
|
||||
});
|
||||
});
|
||||
|
||||
1
frontend/pages/ResetPasswordPage/index.js
Normal file
1
frontend/pages/ResetPasswordPage/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default from './ResetPasswordPage';
|
||||
|
|
@ -9,6 +9,7 @@ import HomePage from '../pages/HomePage';
|
|||
import LoginPage from '../pages/LoginPage';
|
||||
import LoginSuccessfulPage from '../pages/LoginSuccessfulPage';
|
||||
import LoginRoutes from '../components/LoginRoutes';
|
||||
import ResetPasswordPage from '../pages/ResetPasswordPage';
|
||||
import store from '../redux/store';
|
||||
|
||||
const history = syncHistoryWithStore(browserHistory, store);
|
||||
|
|
@ -19,9 +20,10 @@ const routes = (
|
|||
<Route path="/" component={radium(App)}>
|
||||
<IndexRoute component={radium(HomePage)} />
|
||||
<Route component={radium(LoginRoutes)}>
|
||||
<Route path="forgot_password" component={radium(ForgotPasswordPage)} />
|
||||
<Route path="login" component={radium(LoginPage)} />
|
||||
<Route path="login_successful" component={radium(LoginSuccessfulPage)} />
|
||||
<Route path="forgot_password" component={radium(ForgotPasswordPage)} />
|
||||
<Route path="reset_password" component={radium(ResetPasswordPage)} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,30 @@
|
|||
const grey = '#66696f';
|
||||
|
||||
export default {
|
||||
accentLight: '#EAEDFB',
|
||||
brightPurple: '#AE6DDF',
|
||||
darkGrey: '#202532',
|
||||
green: '#4FD061',
|
||||
grey,
|
||||
lightGrey: '#B4B4B4',
|
||||
link: '#4A90E2',
|
||||
logoPurple: '#9651CA',
|
||||
mediumGrey: '#6F737F',
|
||||
primary: grey,
|
||||
purple: '#c38dec',
|
||||
purpleGrey: '#858495',
|
||||
red: '#FF5850',
|
||||
successLight: '#94E39F',
|
||||
white: '#FFF',
|
||||
|
||||
// Correct Colors
|
||||
brand: '#AE6DDF',
|
||||
brandDark: '#9651CA',
|
||||
brandLight: '#C38DEC',
|
||||
brandUltralight: '#EDD6FF',
|
||||
accentText: '#A8B1CD',
|
||||
accentDark: '#B8C2E3',
|
||||
accentMedium: '#D2DAF4',
|
||||
accentLight: '#EAEDFB',
|
||||
textUltradark: '#66696F',
|
||||
textDark: '#858495',
|
||||
textMedium: '#9CA3AC',
|
||||
textLight: '#B2BBC6',
|
||||
bgMedium: '#F4F6FB',
|
||||
bgLight: '#FCFCFF',
|
||||
link: '#4A90E2',
|
||||
linkLight: '#C0D8F5',
|
||||
alert: '#FF5850',
|
||||
alertLight: '#FFB5B2',
|
||||
success: '#4FD061',
|
||||
successLight: '#94E39F',
|
||||
warning: '#FFAD00',
|
||||
warningLight: '#FFDA8C',
|
||||
footerBg: '#202532',
|
||||
footerAccent: '#84878E',
|
||||
borderMedium: '#D4D8DF',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ export default {
|
|||
minHeight: '100%',
|
||||
},
|
||||
body: {
|
||||
color: color.primary,
|
||||
backgroundImage: 'url("/assets/images/background.png")',
|
||||
backgroundSize: 'cover',
|
||||
color: color.textUltradark,
|
||||
...defaultMargin,
|
||||
...defaultPadding,
|
||||
fontFamily: 'Oxygen, sans-serif',
|
||||
|
|
@ -26,15 +28,4 @@ export default {
|
|||
'h1, h2, h3': {
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
'#app': {
|
||||
},
|
||||
'#bg': {
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zIndex: '-1',
|
||||
opacity: '0.4',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
<title>Kolide</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="bg"></div>
|
||||
<div id="app"></div>
|
||||
<script async defer src="/assets/bundle.js" onload="this.parentElement.removeChild(this)"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,155 +0,0 @@
|
|||
/* eslint-disable no-mixed-operators */
|
||||
const { document, window } = global;
|
||||
const refreshDuration = 10000;
|
||||
const SHAPE_DENSITY = 20;
|
||||
|
||||
let numPointsX;
|
||||
let numPointsY;
|
||||
let points;
|
||||
let refreshTimeout;
|
||||
let unitHeight;
|
||||
let unitWidth;
|
||||
|
||||
const randomize = () => {
|
||||
const { length: pointsLength } = points;
|
||||
|
||||
for (let i = 0; i < pointsLength; i++) {
|
||||
const { originX, originY } = points[i];
|
||||
|
||||
if (originX !== 0 && originX !== (unitWidth * (numPointsX - 1))) {
|
||||
points[i].x = originX + Math.random() * unitWidth - unitWidth / 2;
|
||||
}
|
||||
|
||||
if (originY !== 0 && originY !== (unitHeight * (numPointsY - 1))) {
|
||||
points[i].y = originY + Math.random() * unitHeight - unitHeight / 2;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
randomize();
|
||||
|
||||
const svgElement = document.querySelector('#bg svg');
|
||||
const childNodes = svgElement.childNodes;
|
||||
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const polygon = childNodes[i];
|
||||
const animate = polygon.childNodes[0];
|
||||
const point1 = points[polygon.point1];
|
||||
const point2 = points[polygon.point2];
|
||||
const point3 = points[polygon.point3];
|
||||
|
||||
if (animate.getAttribute('to')) {
|
||||
animate.setAttribute('from', animate.getAttribute('to'));
|
||||
}
|
||||
|
||||
animate.setAttribute('to', `${point1.x},${point1.y} ${point2.x},${point2.y} ${point3.x},${point3.y}`);
|
||||
animate.beginElement();
|
||||
}
|
||||
|
||||
refreshTimeout = setTimeout(refresh, refreshDuration);
|
||||
};
|
||||
|
||||
export const loadBackground = () => {
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
const appElement = document.querySelector('#bg');
|
||||
const { innerWidth } = window;
|
||||
const innerHeight = window.innerHeight;
|
||||
const unitSize = (innerWidth + innerHeight) / SHAPE_DENSITY;
|
||||
svg.setAttribute('width', innerWidth);
|
||||
svg.setAttribute('height', innerHeight);
|
||||
appElement.appendChild(svg);
|
||||
|
||||
numPointsX = Math.ceil(innerWidth / unitSize) + 1;
|
||||
numPointsY = Math.ceil(innerHeight / unitSize) + 1;
|
||||
unitWidth = Math.ceil(innerWidth / (numPointsX - 1));
|
||||
unitHeight = Math.ceil(innerHeight / (numPointsY - 1));
|
||||
|
||||
points = [];
|
||||
|
||||
for (let y = 0; y < numPointsY; y++) {
|
||||
for (let x = 0; x < numPointsX; x++) {
|
||||
const originX = unitWidth * x;
|
||||
const originY = unitHeight * y;
|
||||
|
||||
points.push({
|
||||
x: originX,
|
||||
y: originY,
|
||||
originX,
|
||||
originY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
randomize();
|
||||
|
||||
const { length: pointsLength } = points;
|
||||
|
||||
for (let i = 0; i < pointsLength; i++) {
|
||||
const { originX, originY } = points[i];
|
||||
|
||||
if (originX !== unitWidth * (numPointsX - 1) && originY !== unitHeight * (numPointsY - 1)) {
|
||||
const { x: topLeftX, y: topLeftY } = points[i];
|
||||
const { x: topRightX, y: topRightY } = points[i + 1];
|
||||
const { x: bottomLeftX, y: bottomLeftY } = points[i + numPointsX];
|
||||
const { x: bottomRightX, y: bottomRightY } = points[i + numPointsX + 1];
|
||||
|
||||
const rando = Math.floor(Math.random() * 2);
|
||||
|
||||
for (let n = 0; n < 2; n++) {
|
||||
const polygon = document.createElementNS(svg.namespaceURI, 'polygon');
|
||||
|
||||
if (rando === 0) {
|
||||
if (n === 0) {
|
||||
polygon.point1 = i;
|
||||
polygon.point2 = i + numPointsX;
|
||||
polygon.point3 = i + numPointsX + 1;
|
||||
polygon.setAttribute('points', `${topLeftX},${topLeftY} ${bottomLeftX},${bottomLeftY} ${bottomRightX},${bottomRightY}`);
|
||||
} else if (n === 1) {
|
||||
polygon.point1 = i;
|
||||
polygon.point2 = i + 1;
|
||||
polygon.point3 = i + numPointsX + 1;
|
||||
polygon.setAttribute('points', `${topLeftX},${topLeftY} ${topRightX},${topRightY} ${bottomRightX},${bottomRightY}`);
|
||||
}
|
||||
} else if (rando === 1) {
|
||||
if (n === 0) {
|
||||
polygon.point1 = i;
|
||||
polygon.point2 = i + numPointsX;
|
||||
polygon.point3 = i + 1;
|
||||
polygon.setAttribute('points', `${topLeftX},${topLeftY} ${bottomLeftX},${bottomLeftY} ${topRightX},${topRightY}`);
|
||||
} else if (n === 1) {
|
||||
polygon.point1 = i + numPointsX;
|
||||
polygon.point2 = i + 1;
|
||||
polygon.point3 = i + numPointsX + 1;
|
||||
polygon.setAttribute('points', `${bottomLeftX},${bottomLeftY} ${topRightX},${topRightY} ${bottomRightX},${bottomRightY}`);
|
||||
}
|
||||
}
|
||||
|
||||
polygon.setAttribute('fill', `rgba(0, 0, 0, ${Math.random() / 3})`);
|
||||
|
||||
const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
|
||||
|
||||
animate.setAttribute('fill', 'freeze');
|
||||
animate.setAttribute('attributeName', 'points');
|
||||
animate.setAttribute('dur', `${refreshDuration}ms`);
|
||||
animate.setAttribute('calcMode', 'linear');
|
||||
polygon.appendChild(animate);
|
||||
svg.appendChild(polygon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
};
|
||||
|
||||
export const removeBackground = () => {
|
||||
if (document.querySelector('#bg svg')) {
|
||||
document.querySelector('#bg svg').remove();
|
||||
clearTimeout(refreshTimeout);
|
||||
}
|
||||
};
|
||||
|
||||
export const resizeBackground = () => {
|
||||
removeBackground();
|
||||
loadBackground();
|
||||
};
|
||||
Loading…
Reference in a new issue