mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-31 02:26:50 +10:00
commit
2347354014
23 changed files with 614 additions and 257 deletions
|
|
@ -45,8 +45,8 @@ describe('Application', () => {
|
|||
'/signup': 'Sign up',
|
||||
'/password': 'Request new password',
|
||||
'/password/forgotten': 'Request new password',
|
||||
'/password/change': 'Type new password',
|
||||
'/this-url-should-not-be-found': 'Page not found',
|
||||
'/reset-password?t=token&e=email': 'Reset password',
|
||||
};
|
||||
forEach(urlTitles, (title, url) => {
|
||||
const context = {};
|
||||
|
|
|
|||
57
src/components/KeysIcon/KeysIcon.js
Normal file
57
src/components/KeysIcon/KeysIcon.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
|
||||
const KeysIcon = props => {
|
||||
const { className } = props;
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="50px"
|
||||
height="57px"
|
||||
viewBox="0 0 50 57"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<g transform="translate(-538.000000, -240.000000)" stroke="#C0392B">
|
||||
<g transform="translate(540.000000, 242.000000)">
|
||||
<g>
|
||||
<path
|
||||
d="M25,19.65 C25,12.6626 19.4022727,7 12.5,7 C5.59772727,7 0,12.6626 0,19.65 C0,25.4391 3.84772727,30.3082 9.09090909,31.8101 L9.09090909,34.6 L6.81818182,36.8977 L9.09090909,39.2 L9.09090909,41.5 L6.81818182,43.8 L9.09090909,46.1 L9.09090909,49.55 L12.5,53 L15.9090909,49.55 L15.9090909,31.8101 C21.1522727,30.3082 25,25.4391 25,19.65 L25,19.65 Z"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M16,15.5 C16,17.432 14.432,19 12.5,19 C10.568,19 9,17.432 9,15.5 C9,13.568 10.568,12 12.5,12 C14.432,12 16,13.568 16,15.5 L16,15.5 Z"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M16,33.460821 C19.8584615,35.6660894 24.6353846,35.6843714 28.5192308,33.5362343 L31,35.7163649 L31,39.1442433 L34.4615385,39.1442433 L35.6153846,40.3440007 L35.6153846,43.7147478 L38.6546154,43.7147478 L41.1053846,46 L46,46 L46,41.15298 L33.4138462,28.6892143 C36.1138462,23.9061813 35.4192308,17.7474266 31.3138462,13.6819628 C28.42,10.8162565 24.4784615,9.62335484 20.71,10.1032578"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M13,15 L13,4.61538462 C13,2.06769231 15.016,0 17.5,0 C19.984,0 22,2.06769231 22,4.61538462 L22,9.81230769"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
KeysIcon.defaultProps = { className: null };
|
||||
|
||||
const { string } = PropTypes;
|
||||
|
||||
KeysIcon.propTypes = {
|
||||
className: string,
|
||||
};
|
||||
|
||||
export default KeysIcon;
|
||||
57
src/components/KeysIconSuccess/KeysIconSuccess.js
Normal file
57
src/components/KeysIconSuccess/KeysIconSuccess.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
|
||||
const KeysIconSuccess = props => {
|
||||
const { className } = props;
|
||||
return (
|
||||
<svg className={className} width="52" height="60" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(2 2)" fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M25 19.6C25 12.6 19.4 7 12.5 7 5.5 7 0 12.7 0 19.6c0 5.8 3.8 10.7 9 12.2v2.8L7 37 9 39v2.3L7 43.8 9 46v3.5l3.5 3.5 3.4-3.5V31.8c5.2-1.5 9-6.4 9-12.2z"
|
||||
stroke="#C0392B"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M16 15.5c0 2-1.6 3.5-3.5 3.5-2 0-3.5-1.6-3.5-3.5 0-2 1.6-3.5 3.5-3.5 2 0 3.5 1.6 3.5 3.5z"
|
||||
stroke="#C0392B"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M16 33.5c4 2.2 8.6 2.2 12.5 0l2.5 2.2V39h3.5l1 1.3v3.4h3.2L41 46h5v-4.8L33.4 28.7c2.7-4.8 2-11-2-15-3-3-7-4-10.7-3.6"
|
||||
stroke="#C0392B"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13 15V4.6C13 2 15 0 17.5 0S22 2 22 4.6v5.2"
|
||||
stroke="#C0392B"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle stroke="#FFF" strokeWidth="2" fill="#2ECC71" cx="35.5" cy="43.5" r="13.5" />
|
||||
<path
|
||||
stroke="#FFF"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M42 41l-8 8-4-4"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
KeysIconSuccess.defaultProps = { className: null };
|
||||
|
||||
const { string } = PropTypes;
|
||||
|
||||
KeysIconSuccess.propTypes = {
|
||||
className: string,
|
||||
};
|
||||
|
||||
export default KeysIconSuccess;
|
||||
|
|
@ -19,6 +19,8 @@ import FilterPanel from './FilterPanel/FilterPanel';
|
|||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ImageCarousel from './ImageCarousel/ImageCarousel';
|
||||
import ImageFromFile from './ImageFromFile/ImageFromFile';
|
||||
import KeysIcon from './KeysIcon/KeysIcon';
|
||||
import KeysIconSuccess from './KeysIconSuccess/KeysIconSuccess';
|
||||
import ListingCard from './ListingCard/ListingCard';
|
||||
import LocationAutocompleteInput, {
|
||||
LocationAutocompleteInputField,
|
||||
|
|
@ -88,6 +90,8 @@ export {
|
|||
ImageCarousel,
|
||||
ImageFromFile,
|
||||
InlineTextButton,
|
||||
KeysIcon,
|
||||
KeysIconSuccess,
|
||||
ListingCard,
|
||||
LocationAutocompleteInput,
|
||||
LocationAutocompleteInputField,
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
/* eslint-disable no-console */
|
||||
import ChangePasswordForm from './ChangePasswordForm';
|
||||
|
||||
export const Empty = {
|
||||
component: ChangePasswordForm,
|
||||
props: {
|
||||
onSubmit(values) {
|
||||
console.log('submit new password form values:', values);
|
||||
},
|
||||
},
|
||||
group: 'forms',
|
||||
};
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
|
||||
import { Button } from '../../components';
|
||||
|
||||
const ChangePasswordForm = props => {
|
||||
const { handleSubmit, pristine, submitting } = props;
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label htmlFor="newPassword1">New password</label>
|
||||
<Field name="newPassword1" component="input" type="password" />
|
||||
<label htmlFor="newPassword2">New password, again</label>
|
||||
<Field name="newPassword2" component="input" type="password" />
|
||||
<Button type="submit" disabled={pristine || submitting}>Change password</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
ChangePasswordForm.propTypes = { ...formPropTypes };
|
||||
|
||||
const defaultFormName = 'ChangePasswordForm';
|
||||
|
||||
export default reduxForm({ form: defaultFormName })(ChangePasswordForm);
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import React from 'react';
|
||||
import { renderDeep } from '../../util/test-helpers';
|
||||
import ChangePasswordForm from './ChangePasswordForm';
|
||||
|
||||
describe('ChangePasswordForm', () => {
|
||||
it('matches snapshot', () => {
|
||||
const tree = renderDeep(<ChangePasswordForm />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
exports[`ChangePasswordForm matches snapshot 1`] = `
|
||||
<form
|
||||
onSubmit={[Function]}>
|
||||
<label
|
||||
htmlFor="newPassword1">
|
||||
New password
|
||||
</label>
|
||||
<input
|
||||
name="newPassword1"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onDragStart={[Function]}
|
||||
onDrop={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="password"
|
||||
value="" />
|
||||
<label
|
||||
htmlFor="newPassword2">
|
||||
New password, again
|
||||
</label>
|
||||
<input
|
||||
name="newPassword2"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onDragStart={[Function]}
|
||||
onDrop={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="password"
|
||||
value="" />
|
||||
<button
|
||||
className=""
|
||||
disabled={true}
|
||||
type="submit">
|
||||
Change password
|
||||
</button>
|
||||
</form>
|
||||
`;
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
|
||||
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
|
||||
import { PageLayout, Topbar } from '../../components';
|
||||
import { ChangePasswordForm } from '../../containers';
|
||||
|
||||
const changePassword = values => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('submit with values:', values);
|
||||
};
|
||||
|
||||
export const PasswordChangePageComponent = props => {
|
||||
const {
|
||||
authInfoError,
|
||||
authInProgress,
|
||||
currentUser,
|
||||
currentUserHasListings,
|
||||
history,
|
||||
isAuthenticated,
|
||||
location,
|
||||
logoutError,
|
||||
notificationCount,
|
||||
onLogout,
|
||||
onManageDisableScrolling,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title="Type new password">
|
||||
<Topbar
|
||||
authInProgress={authInProgress}
|
||||
currentUser={currentUser}
|
||||
currentUserHasListings={currentUserHasListings}
|
||||
history={history}
|
||||
isAuthenticated={isAuthenticated}
|
||||
location={location}
|
||||
notificationCount={notificationCount}
|
||||
onLogout={onLogout}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
/>
|
||||
<ChangePasswordForm onSubmit={changePassword} />
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
PasswordChangePageComponent.defaultProps = {
|
||||
authInfoError: null,
|
||||
currentUser: null,
|
||||
logoutError: null,
|
||||
notificationCount: 0,
|
||||
};
|
||||
|
||||
const { bool, func, instanceOf, number, object, shape } = PropTypes;
|
||||
|
||||
PasswordChangePageComponent.propTypes = {
|
||||
authInfoError: instanceOf(Error),
|
||||
authInProgress: bool.isRequired,
|
||||
currentUser: propTypes.currentUser,
|
||||
currentUserHasListings: bool.isRequired,
|
||||
isAuthenticated: bool.isRequired,
|
||||
logoutError: instanceOf(Error),
|
||||
notificationCount: number,
|
||||
onLogout: func.isRequired,
|
||||
onManageDisableScrolling: func.isRequired,
|
||||
|
||||
// from withRouter
|
||||
history: shape({
|
||||
push: func.isRequired,
|
||||
}).isRequired,
|
||||
location: shape({ state: object }).isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
// PageLayout needs authInfoError and logoutError, Topbar needs isAuthenticated
|
||||
const { authInfoError, isAuthenticated, logoutError } = state.Auth;
|
||||
// Topbar needs user info.
|
||||
const {
|
||||
currentUser,
|
||||
currentUserHasListings,
|
||||
currentUserNotificationCount: notificationCount,
|
||||
} = state.user;
|
||||
return {
|
||||
authInfoError,
|
||||
authInProgress: authenticationInProgress(state),
|
||||
currentUser,
|
||||
currentUserHasListings,
|
||||
currentUserNotificationCount: notificationCount,
|
||||
isAuthenticated,
|
||||
logoutError,
|
||||
scrollingDisabled: isScrollingDisabled(state),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onLogout: historyPush => dispatch(logout(historyPush)),
|
||||
onManageDisableScrolling: (componentId, disableScrolling) =>
|
||||
dispatch(manageDisableScrolling(componentId, disableScrolling)),
|
||||
});
|
||||
|
||||
const PasswordChangePage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)(
|
||||
PasswordChangePageComponent
|
||||
);
|
||||
|
||||
export default PasswordChangePage;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import React from 'react';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { PasswordChangePageComponent } from './PasswordChangePage';
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
describe('ContactDetailsPage', () => {
|
||||
it('matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<PasswordChangePageComponent
|
||||
params={{ displayName: 'my-shop' }}
|
||||
history={{ push: noop }}
|
||||
location={{ search: '' }}
|
||||
scrollingDisabled={false}
|
||||
authInProgress={false}
|
||||
currentUserHasListings={false}
|
||||
isAuthenticated={false}
|
||||
onLogout={noop}
|
||||
onManageDisableScrolling={noop}
|
||||
/>
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
exports[`ContactDetailsPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
title="Type new password">
|
||||
<Topbar
|
||||
authInProgress={false}
|
||||
currentUser={null}
|
||||
currentUserHasListings={false}
|
||||
history={
|
||||
Object {
|
||||
"push": [Function],
|
||||
}
|
||||
}
|
||||
isAuthenticated={false}
|
||||
location={
|
||||
Object {
|
||||
"search": "",
|
||||
}
|
||||
}
|
||||
notificationCount={0}
|
||||
onLogout={[Function]}
|
||||
onManageDisableScrolling={[Function]} />
|
||||
<ReduxForm
|
||||
onSubmit={[Function]} />
|
||||
</withRouter(PageLayout)>
|
||||
`;
|
||||
15
src/containers/PasswordResetForm/PasswordResetForm.css
Normal file
15
src/containers/PasswordResetForm/PasswordResetForm.css
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.root {
|
||||
|
||||
}
|
||||
|
||||
.passwordInput {
|
||||
/* Leave space between the input and the button below when the
|
||||
viewport height is small */
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
/* In a flexbox context, this makes the button position itself to the
|
||||
bottom */
|
||||
margin-top: auto;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/* eslint-disable no-console */
|
||||
import PasswordResetForm from './PasswordResetForm';
|
||||
|
||||
export const Empty = {
|
||||
component: PasswordResetForm,
|
||||
props: {
|
||||
onSubmit(values) {
|
||||
console.log('submit with values:', values);
|
||||
},
|
||||
},
|
||||
group: 'forms',
|
||||
};
|
||||
89
src/containers/PasswordResetForm/PasswordResetForm.js
Normal file
89
src/containers/PasswordResetForm/PasswordResetForm.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
||||
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
|
||||
import classNames from 'classnames';
|
||||
import { PrimaryButton, TextInputField } from '../../components';
|
||||
import * as validators from '../../util/validators';
|
||||
|
||||
import css from './PasswordResetForm.css';
|
||||
|
||||
const PASSWORD_MIN_LENGTH = 8;
|
||||
|
||||
const PasswordResetFormComponent = props => {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
form,
|
||||
handleSubmit,
|
||||
submitting,
|
||||
inProgress,
|
||||
intl,
|
||||
invalid,
|
||||
} = props;
|
||||
|
||||
// password
|
||||
const passwordLabel = intl.formatMessage({
|
||||
id: 'PasswordResetForm.passwordLabel',
|
||||
});
|
||||
const passwordPlaceholder = intl.formatMessage({
|
||||
id: 'PasswordResetForm.passwordPlaceholder',
|
||||
});
|
||||
const passwordRequiredMessage = intl.formatMessage({
|
||||
id: 'PasswordResetForm.passwordRequired',
|
||||
});
|
||||
const passwordMinLengthMessage = intl.formatMessage(
|
||||
{
|
||||
id: 'PasswordResetForm.passwordTooShort',
|
||||
},
|
||||
{
|
||||
minLength: PASSWORD_MIN_LENGTH,
|
||||
}
|
||||
);
|
||||
const passwordRequired = validators.required(passwordRequiredMessage);
|
||||
const passwordMinLength = validators.minLength(passwordMinLengthMessage, PASSWORD_MIN_LENGTH);
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
const submitDisabled = invalid || submitting || inProgress;
|
||||
|
||||
return (
|
||||
<form className={classes} onSubmit={handleSubmit}>
|
||||
<TextInputField
|
||||
className={css.passwordInput}
|
||||
type="password"
|
||||
name="password"
|
||||
id={`${form}.password`}
|
||||
label={passwordLabel}
|
||||
placeholder={passwordPlaceholder}
|
||||
validate={[passwordRequired, passwordMinLength]}
|
||||
/>
|
||||
<PrimaryButton className={css.submitButton} type="submit" disabled={submitDisabled}>
|
||||
<FormattedMessage id="PasswordResetForm.submitButtonText" />
|
||||
</PrimaryButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
PasswordResetFormComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
inProgress: false,
|
||||
};
|
||||
|
||||
const { string, bool } = PropTypes;
|
||||
|
||||
PasswordResetFormComponent.propTypes = {
|
||||
...formPropTypes,
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
inProgress: bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const defaultFormName = 'PasswordResetForm';
|
||||
|
||||
const PasswordResetForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
|
||||
PasswordResetFormComponent
|
||||
);
|
||||
|
||||
export default PasswordResetForm;
|
||||
70
src/containers/PasswordResetPage/PasswordResetPage.css
Normal file
70
src/containers/PasswordResetPage/PasswordResetPage.css
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.root {
|
||||
@apply --backgroundImage;
|
||||
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 30px 24px 98px 24px;
|
||||
background-color: var(--matterColorLight);
|
||||
border-radius: 2px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
flex-basis: 480px;
|
||||
flex-grow: 0;
|
||||
min-height: 573px;
|
||||
padding: 60px;
|
||||
margin-top: 7.5vh;
|
||||
margin-bottom: 7.5vh;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--failColor);
|
||||
}
|
||||
|
||||
.mainHeading {
|
||||
margin-top: 26px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-top: 12px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 37px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonLink {
|
||||
@apply --marketplaceButtonStylesPrimary;
|
||||
|
||||
/* Pull the button to the bottom of the flex container */
|
||||
margin-top: auto;
|
||||
}
|
||||
51
src/containers/PasswordResetPage/PasswordResetPage.duck.js
Normal file
51
src/containers/PasswordResetPage/PasswordResetPage.duck.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// ================ Action types ================ //
|
||||
|
||||
export const RESET_PASSWORD_REQUEST = 'app/PasswordResetPage/RESET_PASSWORD_REQUEST';
|
||||
export const RESET_PASSWORD_SUCCESS = 'app/PasswordResetPage/RESET_PASSWORD_SUCCESS';
|
||||
export const RESET_PASSWORD_ERROR = 'app/PasswordResetPage/RESET_PASSWORD_ERROR';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
resetPasswordInProgress: false,
|
||||
resetPasswordError: null,
|
||||
};
|
||||
|
||||
export default function reducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case RESET_PASSWORD_REQUEST:
|
||||
return { ...state, resetPasswordInProgress: true, resetPasswordError: null };
|
||||
case RESET_PASSWORD_SUCCESS:
|
||||
return { ...state, resetPasswordInProgress: false };
|
||||
case RESET_PASSWORD_ERROR:
|
||||
console.error(payload); // eslint-disable-line no-console
|
||||
return { ...state, resetPasswordInProgress: false, resetPasswordError: payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// ================ Action creators ================ //
|
||||
|
||||
export const resetPasswordRequest = () => ({ type: RESET_PASSWORD_REQUEST });
|
||||
|
||||
export const resetPasswordSuccess = () => ({ type: RESET_PASSWORD_SUCCESS });
|
||||
|
||||
export const resetPasswordError = e => ({
|
||||
type: RESET_PASSWORD_ERROR,
|
||||
error: true,
|
||||
payload: e,
|
||||
});
|
||||
|
||||
// ================ Thunks ================ //
|
||||
|
||||
export const resetPassword = (email, passwordResetToken, newPassword) =>
|
||||
(dispatch, getState, sdk) => {
|
||||
dispatch(resetPasswordRequest());
|
||||
const params = { email, passwordResetToken, newPassword };
|
||||
return sdk.passwordReset
|
||||
.reset(params)
|
||||
.then(() => dispatch(resetPasswordSuccess()))
|
||||
.catch(e => dispatch(resetPasswordError(e)));
|
||||
};
|
||||
218
src/containers/PasswordResetPage/PasswordResetPage.js
Normal file
218
src/containers/PasswordResetPage/PasswordResetPage.js
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import React, { PropTypes, Component } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { PageLayout, Topbar, NamedLink, KeysIcon, KeysIconSuccess } from '../../components';
|
||||
import { PasswordResetForm } from '../../containers';
|
||||
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
|
||||
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
|
||||
import { parse } from '../../util/urlHelpers';
|
||||
import { resetPassword } from './PasswordResetPage.duck';
|
||||
|
||||
import css from './PasswordResetPage.css';
|
||||
|
||||
const { bool, func, instanceOf, number, object, shape, string } = PropTypes;
|
||||
|
||||
const parseUrlParams = location => {
|
||||
const params = parse(location.search);
|
||||
const { t: token, e: email } = params;
|
||||
return { token, email };
|
||||
};
|
||||
|
||||
export class PasswordResetPageComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { newPasswordSubmitted: false };
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
authInfoError,
|
||||
authInProgress,
|
||||
currentUser,
|
||||
currentUserHasListings,
|
||||
intl,
|
||||
isAuthenticated,
|
||||
logoutError,
|
||||
notificationCount,
|
||||
onLogout,
|
||||
onManageDisableScrolling,
|
||||
scrollingDisabled,
|
||||
location,
|
||||
history,
|
||||
resetPasswordInProgress,
|
||||
resetPasswordError,
|
||||
onSubmitPassword,
|
||||
} = this.props;
|
||||
|
||||
const title = intl.formatMessage({
|
||||
id: 'PasswordResetPage.title',
|
||||
});
|
||||
|
||||
const { token, email } = parseUrlParams(location);
|
||||
const paramsValid = !!(token && email);
|
||||
|
||||
const handleSubmit = values => {
|
||||
const { password } = values;
|
||||
this.setState({ newPasswordSubmitted: false });
|
||||
onSubmitPassword(email, token, password).then(() => {
|
||||
this.setState({ newPasswordSubmitted: true });
|
||||
});
|
||||
};
|
||||
|
||||
const paramsErrorContent = (
|
||||
<div className={css.content}>
|
||||
<p className={css.error}>
|
||||
<FormattedMessage id="PasswordResetPage.invalidUrlParams" />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const resetFormContent = (
|
||||
<div className={css.content}>
|
||||
<KeysIcon />
|
||||
<h1 className={css.mainHeading}>
|
||||
<FormattedMessage id="PasswordResetPage.mainHeading" />
|
||||
</h1>
|
||||
<p className={css.helpText}>
|
||||
<FormattedMessage id="PasswordResetPage.helpText" />
|
||||
</p>
|
||||
{resetPasswordError
|
||||
? <p className={css.error}>
|
||||
<FormattedMessage id="PasswordResetPage.resetFailed" />
|
||||
</p>
|
||||
: null}
|
||||
<PasswordResetForm
|
||||
className={css.form}
|
||||
onSubmit={handleSubmit}
|
||||
inProgress={resetPasswordInProgress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const resetDoneContent = (
|
||||
<div className={css.content}>
|
||||
<KeysIconSuccess />
|
||||
<h1 className={css.mainHeading}>
|
||||
<FormattedMessage id="PasswordResetPage.passwordChangedHeading" />
|
||||
</h1>
|
||||
<p className={css.helpText}>
|
||||
<FormattedMessage id="PasswordResetPage.passwordChangedHelpText" />
|
||||
</p>
|
||||
<NamedLink name="LoginPage" className={css.buttonLink}>
|
||||
<FormattedMessage id="PasswordResetPage.loginButtonText" />
|
||||
</NamedLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
let content;
|
||||
|
||||
if (!paramsValid) {
|
||||
content = paramsErrorContent;
|
||||
} else if (!resetPasswordError && this.state.newPasswordSubmitted) {
|
||||
content = resetDoneContent;
|
||||
} else {
|
||||
content = resetFormContent;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title={title}
|
||||
authInfoError={authInfoError}
|
||||
logoutError={logoutError}
|
||||
scrollingDisabled={scrollingDisabled}
|
||||
>
|
||||
<Topbar
|
||||
isAuthenticated={isAuthenticated}
|
||||
authInProgress={authInProgress}
|
||||
currentUser={currentUser}
|
||||
currentUserHasListings={currentUserHasListings}
|
||||
notificationCount={notificationCount}
|
||||
history={history}
|
||||
location={location}
|
||||
onLogout={onLogout}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
/>
|
||||
<div className={css.root}>
|
||||
{content}
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PasswordResetPageComponent.defaultProps = {
|
||||
authInfoError: null,
|
||||
currentUser: null,
|
||||
logoutError: null,
|
||||
notificationCount: 0,
|
||||
resetPasswordError: null,
|
||||
};
|
||||
|
||||
PasswordResetPageComponent.propTypes = {
|
||||
authInfoError: instanceOf(Error),
|
||||
authInProgress: bool.isRequired,
|
||||
currentUser: propTypes.currentUser,
|
||||
currentUserHasListings: bool.isRequired,
|
||||
isAuthenticated: bool.isRequired,
|
||||
logoutError: instanceOf(Error),
|
||||
notificationCount: number,
|
||||
onLogout: func.isRequired,
|
||||
onManageDisableScrolling: func.isRequired,
|
||||
scrollingDisabled: bool.isRequired,
|
||||
|
||||
resetPasswordInProgress: bool.isRequired,
|
||||
resetPasswordError: instanceOf(Error),
|
||||
onSubmitPassword: func.isRequired,
|
||||
|
||||
// from withRouter
|
||||
history: object.isRequired,
|
||||
location: shape({
|
||||
search: string,
|
||||
}).isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const {
|
||||
authInfoError,
|
||||
isAuthenticated,
|
||||
logoutError,
|
||||
} = state.Auth;
|
||||
const {
|
||||
currentUser,
|
||||
currentUserHasListings,
|
||||
currentUserNotificationCount: notificationCount,
|
||||
} = state.user;
|
||||
const { resetPasswordInProgress, resetPasswordError } = state.PasswordResetPage;
|
||||
return {
|
||||
authInfoError,
|
||||
authInProgress: authenticationInProgress(state),
|
||||
currentUser,
|
||||
currentUserHasListings,
|
||||
isAuthenticated,
|
||||
logoutError,
|
||||
notificationCount,
|
||||
scrollingDisabled: isScrollingDisabled(state),
|
||||
resetPasswordInProgress,
|
||||
resetPasswordError,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onLogout: () => dispatch(logout()),
|
||||
onManageDisableScrolling: (componentId, disableScrolling) =>
|
||||
dispatch(manageDisableScrolling(componentId, disableScrolling)),
|
||||
onSubmitPassword: (email, token, password) => dispatch(resetPassword(email, token, password)),
|
||||
});
|
||||
|
||||
const PasswordResetPage = compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withRouter,
|
||||
injectIntl
|
||||
)(PasswordResetPageComponent);
|
||||
|
||||
export default PasswordResetPage;
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import AuthenticationPage from './AuthenticationPage/AuthenticationPage';
|
||||
import BookingDatesForm from './BookingDatesForm/BookingDatesForm';
|
||||
import ChangeAccountPasswordForm from './ChangeAccountPasswordForm/ChangeAccountPasswordForm';
|
||||
import ChangePasswordForm from './ChangePasswordForm/ChangePasswordForm';
|
||||
import CheckoutPage from './CheckoutPage/CheckoutPage';
|
||||
import ContactDetailsPage from './ContactDetailsPage/ContactDetailsPage';
|
||||
import EditListingDescriptionForm from './EditListingDescriptionForm/EditListingDescriptionForm';
|
||||
|
|
@ -9,8 +8,8 @@ import EditListingLocationForm from './EditListingLocationForm/EditListingLocati
|
|||
import EditListingPage from './EditListingPage/EditListingPage';
|
||||
import EditListingPhotosForm from './EditListingPhotosForm/EditListingPhotosForm';
|
||||
import EditListingPricingForm from './EditListingPricingForm/EditListingPricingForm';
|
||||
import EmailVerificationPage from './EmailVerificationPage/EmailVerificationPage';
|
||||
import EmailVerificationForm from './EmailVerificationForm/EmailVerificationForm';
|
||||
import EmailVerificationPage from './EmailVerificationPage/EmailVerificationPage';
|
||||
import InboxPage from './InboxPage/InboxPage';
|
||||
import LandingPage from './LandingPage/LandingPage';
|
||||
import ListingPage from './ListingPage/ListingPage';
|
||||
|
|
@ -19,9 +18,10 @@ import LoginForm from './LoginForm/LoginForm';
|
|||
import ManageListingsPage from './ManageListingsPage/ManageListingsPage';
|
||||
import NotFoundPage from './NotFoundPage/NotFoundPage';
|
||||
import OrderPage from './OrderPage/OrderPage';
|
||||
import PasswordChangePage from './PasswordChangePage/PasswordChangePage';
|
||||
import PasswordForgottenForm from './PasswordForgottenForm/PasswordForgottenForm';
|
||||
import PasswordForgottenPage from './PasswordForgottenPage/PasswordForgottenPage';
|
||||
import PasswordResetForm from './PasswordResetForm/PasswordResetForm';
|
||||
import PasswordResetPage from './PasswordResetPage/PasswordResetPage';
|
||||
import PayoutDetailsForm from './PayoutDetailsForm/PayoutDetailsForm';
|
||||
import PayoutPreferencesPage from './PayoutPreferencesPage/PayoutPreferencesPage';
|
||||
import ProfilePage from './ProfilePage/ProfilePage';
|
||||
|
|
@ -39,7 +39,6 @@ export {
|
|||
AuthenticationPage,
|
||||
BookingDatesForm,
|
||||
ChangeAccountPasswordForm,
|
||||
ChangePasswordForm,
|
||||
CheckoutPage,
|
||||
ContactDetailsPage,
|
||||
EditListingDescriptionForm,
|
||||
|
|
@ -47,8 +46,8 @@ export {
|
|||
EditListingPage,
|
||||
EditListingPhotosForm,
|
||||
EditListingPricingForm,
|
||||
EmailVerificationPage,
|
||||
EmailVerificationForm,
|
||||
EmailVerificationPage,
|
||||
InboxPage,
|
||||
LandingPage,
|
||||
ListingPage,
|
||||
|
|
@ -57,9 +56,10 @@ export {
|
|||
ManageListingsPage,
|
||||
NotFoundPage,
|
||||
OrderPage,
|
||||
PasswordChangePage,
|
||||
PasswordForgottenForm,
|
||||
PasswordForgottenPage,
|
||||
PasswordResetForm,
|
||||
PasswordResetPage,
|
||||
PayoutDetailsForm,
|
||||
PayoutPreferencesPage,
|
||||
ProfilePage,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ import CheckoutPage from './CheckoutPage/CheckoutPage.duck';
|
|||
import EditListingPage from './EditListingPage/EditListingPage.duck';
|
||||
import InboxPage from './InboxPage/InboxPage.duck';
|
||||
import ListingPage from './ListingPage/ListingPage.duck';
|
||||
import ManageListingsPage from './ManageListingsPage/ManageListingsPage.duck';
|
||||
import OrderPage from './OrderPage/OrderPage.duck';
|
||||
import PasswordResetPage from './PasswordResetPage/PasswordResetPage.duck';
|
||||
import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage.duck';
|
||||
import SalePage from './SalePage/SalePage.duck';
|
||||
import SearchPage from './SearchPage/SearchPage.duck';
|
||||
import ManageListingsPage from './ManageListingsPage/ManageListingsPage.duck';
|
||||
import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage.duck';
|
||||
|
||||
export {
|
||||
CheckoutPage,
|
||||
|
|
@ -20,6 +21,7 @@ export {
|
|||
ListingPage,
|
||||
ManageListingsPage,
|
||||
OrderPage,
|
||||
PasswordResetPage,
|
||||
ProfileSettingsPage,
|
||||
SalePage,
|
||||
SearchPage,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import * as TopbarDesktop from './components/TopbarDesktop/TopbarDesktop.example
|
|||
import * as BookingDatesForm from './containers/BookingDatesForm/BookingDatesForm.example';
|
||||
import * as ChangeAccountPasswordForm
|
||||
from './containers/ChangeAccountPasswordForm/ChangeAccountPasswordForm.example';
|
||||
import * as ChangePasswordForm from './containers/ChangePasswordForm/ChangePasswordForm.example';
|
||||
import * as Colors from './containers/StyleguidePage/Colors.example';
|
||||
import * as EditListingDescriptionForm
|
||||
from './containers/EditListingDescriptionForm/EditListingDescriptionForm.example';
|
||||
|
|
@ -45,6 +44,7 @@ import * as EditListingPricingForm
|
|||
import * as LoginForm from './containers/LoginForm/LoginForm.example';
|
||||
import * as PasswordForgottenForm
|
||||
from './containers/PasswordForgottenForm/PasswordForgottenForm.example';
|
||||
import * as PasswordResetForm from './containers/PasswordResetForm/PasswordResetForm.example';
|
||||
import * as PayoutDetailsForm from './containers/PayoutDetailsForm/PayoutDetailsForm.example';
|
||||
import * as SignupForm from './containers/SignupForm/SignupForm.example';
|
||||
import * as StripePaymentForm from './containers/StripePaymentForm/StripePaymentForm.example';
|
||||
|
|
@ -57,7 +57,6 @@ export {
|
|||
BookingDatesForm,
|
||||
Button,
|
||||
ChangeAccountPasswordForm,
|
||||
ChangePasswordForm,
|
||||
Colors,
|
||||
CurrencyInputField,
|
||||
DateInputField,
|
||||
|
|
@ -80,6 +79,7 @@ export {
|
|||
NamedLink,
|
||||
PaginationLinks,
|
||||
PasswordForgottenForm,
|
||||
PasswordResetForm,
|
||||
PayoutDetailsForm,
|
||||
ResponsiveImage,
|
||||
SelectField,
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import {
|
|||
ManageListingsPage,
|
||||
NotFoundPage,
|
||||
OrderPage,
|
||||
PasswordChangePage,
|
||||
PasswordForgottenPage,
|
||||
PasswordResetPage,
|
||||
PayoutPreferencesPage,
|
||||
ProfilePage,
|
||||
ProfileSettingsPage,
|
||||
|
|
@ -156,12 +156,6 @@ const routesConfiguration = [
|
|||
name: 'PasswordForgottenPage',
|
||||
component: props => <PasswordForgottenPage {...props} />,
|
||||
},
|
||||
{
|
||||
path: '/password/change',
|
||||
exact: true,
|
||||
name: 'PasswordChangePage',
|
||||
component: props => <PasswordChangePage {...props} />,
|
||||
},
|
||||
{
|
||||
path: '/inbox',
|
||||
auth: true,
|
||||
|
|
@ -302,6 +296,16 @@ const routesConfiguration = [
|
|||
component: props => <NotFoundPage {...props} />,
|
||||
},
|
||||
|
||||
// Do not change this path!
|
||||
//
|
||||
// The API expects that the Starter App implements /reset-password endpoint
|
||||
{
|
||||
path: '/reset-password',
|
||||
exact: true,
|
||||
name: 'PasswordResetPage',
|
||||
component: props => <PasswordResetPage {...props} />,
|
||||
},
|
||||
|
||||
// Do not change this path!
|
||||
//
|
||||
// The API expects that the Starter App implements /verify-email endpoint
|
||||
|
|
|
|||
|
|
@ -205,6 +205,18 @@
|
|||
"PaginationLinks.next": "Next page",
|
||||
"PaginationLinks.previous": "Previous page",
|
||||
"PaginationLinks.toPage": "Go to page {page}",
|
||||
"PasswordResetForm.passwordLabel": "Your new password",
|
||||
"PasswordResetForm.passwordPlaceholder": "Enter your new password...",
|
||||
"PasswordResetForm.passwordRequired": "This field is required",
|
||||
"PasswordResetForm.passwordTooShort": "Password should be at least {minLength} characters",
|
||||
"PasswordResetForm.submitButtonText": "Reset password",
|
||||
"PasswordResetPage.helpText": "Please provide a new password.",
|
||||
"PasswordResetPage.loginButtonText": "Log in",
|
||||
"PasswordResetPage.mainHeading": "Reset your password",
|
||||
"PasswordResetPage.passwordChangedHeading": "Password changed",
|
||||
"PasswordResetPage.passwordChangedHelpText": "Your password has been changed successfully.",
|
||||
"PasswordResetPage.resetFailed": "Reset failed. Please try again.",
|
||||
"PasswordResetPage.title": "Reset password",
|
||||
"PayoutDetailsForm.addressTitle": "Address",
|
||||
"PayoutDetailsForm.bankDetails": "Bank details",
|
||||
"PayoutDetailsForm.birthdayDatePlaceholder": "dd",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ export const required = message =>
|
|||
return value ? VALID : message;
|
||||
};
|
||||
|
||||
export const minLength = (message, minimumLength) =>
|
||||
value => {
|
||||
return value && value.length >= minimumLength ? VALID : message;
|
||||
};
|
||||
|
||||
export const maxLength = (message, maximumLength) =>
|
||||
value => {
|
||||
return !value || value.length <= maximumLength ? VALID : message;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue