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:
Mike Stone 2016-09-16 17:19:37 -04:00 committed by GitHub
parent cfdd665673
commit 482d025d05
30 changed files with 599 additions and 287 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

View file

@ -27,3 +27,8 @@
font-weight: normal;
font-style: normal;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View file

@ -5,7 +5,7 @@ const { color } = styles;
export default {
footerStyles: {
alignItems: 'center',
backgroundColor: color.darkGrey,
backgroundColor: color.footerBg,
height: '55px',
textAlign: 'center',
position: 'absolute',

View file

@ -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;

View 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);

View file

@ -0,0 +1 @@
export default from './StackedWhiteBoxes';

View 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',
},
};

View file

@ -19,7 +19,7 @@ export default {
forgotPasswordStyles: {
fontSize: font.medium,
textDecoration: 'none',
color: color.lightGrey,
color: color.accentText,
},
forgotPasswordWrapperStyles: {
marginTop: padding.base,

View file

@ -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;

View file

@ -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',
});
});
});

View file

@ -0,0 +1 @@
export default from './ResetPasswordForm';

View 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,
},
};

View file

@ -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',
};

View file

@ -0,0 +1,5 @@
import { isEqual } from 'lodash';
export default (actual, expected) => {
return isEqual(actual, expected);
};

View file

@ -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);
});
});

View file

@ -0,0 +1,3 @@
export default (actual) => {
return !!actual;
};

View file

@ -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);
});
});
});

View file

@ -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 youve 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 youve 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>
);
}
}

View file

@ -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',
},
};

View file

@ -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;
}

View file

@ -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: {} });

View file

@ -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,

View 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);

View 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);
});
});

View file

@ -0,0 +1 @@
export default from './ResetPasswordPage';

View file

@ -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>

View file

@ -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',
};

View file

@ -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',
},
};

View file

@ -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>

View file

@ -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();
};