diff --git a/src/components/profile/Profile.jsx b/src/components/profile/Profile.jsx index 9c36f4d..fbd4a61 100644 --- a/src/components/profile/Profile.jsx +++ b/src/components/profile/Profile.jsx @@ -58,6 +58,7 @@ export default class Profile extends Component { /> diff --git a/src/components/profile/changeEmail/ChangeEmail.jsx b/src/components/profile/changeEmail/ChangeEmail.jsx new file mode 100644 index 0000000..f1a128e --- /dev/null +++ b/src/components/profile/changeEmail/ChangeEmail.jsx @@ -0,0 +1,241 @@ +import React, { Component, PropTypes } from 'react'; + +import { FormattedMessage as Message } from 'react-intl'; +import { Link } from 'react-router'; +import classNames from 'classnames'; +import Helmet from 'react-helmet'; +import { Motion, spring } from 'react-motion'; + +import { Input, Button, Form, FormModel } from 'components/ui/form'; +import styles from 'components/profile/profileForm.scss'; +import helpLinks from 'components/auth/helpLinks.scss'; +import MeasureHeight from 'components/MeasureHeight'; + +import changeEmail from './changeEmail.scss'; +import messages from './ChangeEmail.messages'; + +const STEPS_TOTAL = 3; + +// TODO: disable code field, if the code was passed through url + +export default class ChangeEmail extends Component { + static displayName = 'ChangeEmail'; + + static propTypes = { + email: PropTypes.string.isRequired, + form: PropTypes.instanceOf(FormModel), + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired + }; + + static get defaultProps() { + return { + form: new FormModel() + }; + } + + state = { + activeStep: 0 + }; + + + render() { + const {form} = this.props; + const {activeStep} = this.state; + + return ( +
+
+ + +
+
+ + {(pageTitle) => ( +

+ + {pageTitle} +

+ )} +
+ +
+

+ +

+
+
+
+ +
+ {(new Array(STEPS_TOTAL)).fill(0).map((_, step) => ( +
+ ))} +
+ +
+ {this.renderStepForms()} + +
+ +
+ {this.isLastStep() ? null : ( + + + + )} +
+
+ + ); + } + + renderStepForms() { + const {form, email} = this.props; + const {activeStep} = this.state; + + const activeStepHeight = this.state[`step${activeStep}Height`] || 0; + + // a hack to disable height animation on first render + const isHeightMeasured = this.isHeightMeasured; + this.isHeightMeasured = activeStepHeight > 0; + + return ( + + {(interpolatingStyle) => ( +
+
+ +
+
+

+ +

+
+ +
+

+ {email} +

+
+ +
+

+ +

+
+
+
+ + +
+
+

+ {email}) + }} /> +

+
+ +
+ +
+ +
+

+ +

+
+ +
+ +
+
+
+ + +
+
+

+ {form.value('newEmail')}) + }} /> +

+
+ +
+ +
+
+
+
+
+ )} +
+ ); + } + + onStepMeasure(step) { + return (height) => this.setState({ + [`step${step}Height`]: height + }); + } + + onSwitchStep = (event) => { + event.preventDefault(); + + const {activeStep} = this.state; + const nextStep = activeStep + 1; + + if (nextStep < STEPS_TOTAL) { + this.setState({ + activeStep: nextStep + }); + } + }; + + isLastStep() { + return this.state.activeStep + 1 === STEPS_TOTAL; + } + + onUsernameChange = (event) => { + this.props.onChange(event.target.value); + }; + + onFormSubmit = () => { + this.props.onSubmit(this.props.form); + }; +} diff --git a/src/components/profile/changeEmail/ChangeEmail.messages.js b/src/components/profile/changeEmail/ChangeEmail.messages.js new file mode 100644 index 0000000..bbcb7f7 --- /dev/null +++ b/src/components/profile/changeEmail/ChangeEmail.messages.js @@ -0,0 +1,66 @@ +import { defineMessages } from 'react-intl'; + +export default defineMessages({ + changeEmailTitle: { + id: 'changeEmailTitle', + defaultMessage: 'Change E-mail' + // defaultMessage: 'Смена E-mail' + }, + changeEmailDescription: { + id: 'changeEmailDescription', + defaultMessage: 'To change current account E-mail you must first verify that you own the current address and then confirm the new one.' + // defaultMessage: 'Для смены E-mail адреса аккаунта сперва необходимо подтвердить владение текущим адресом, а за тем привязать новый.' + }, + currentAccountEmail: { + id: 'currentAccountEmail', + defaultMessage: 'Current account E-mail address:' + // defaultMessage: 'Текущий E-mail адрес, привязанный к аккаунту:' + }, + pressButtonToStart: { + id: 'pressButtonToStart', + defaultMessage: 'Press the button below to send a message with the code for E-mail change initialization.' + // defaultMessage: 'Нажмите кнопку ниже, что бы отправить письмо с кодом для инциализации процесса смены E-mail адреса.' + }, + enterInitializationCode: { + id: 'enterInitializationCode', + defaultMessage: 'The E-mail with an initialization code for E-mail change procedure was sent to {email}. Please enter the code into the field below:' + // defaultMessage: 'На E-mail {email} было отправлено письмо с кодом для инициализации смены E-mail адреса. Введите его в поле ниже:' + }, +// + enterNewEmail: { + id: 'enterNewEmail', + defaultMessage: 'Then provide your new E-mail address, that you want to use with this account. You will be mailed with confirmation code.' + // defaultMessage: 'За тем укажите новый E-mail адрес, к котором хотите привязать свой аккаунт. На него будет выслан код с подтверждением.' + }, + enterFinalizationCode: { + id: 'enterFinalizationCode', + defaultMessage: 'The E-mail change confirmation code was sent to {email}. Please enter the code received into the field below:' + // defaultMessage: 'На указанный E-mail {email} было выслано письмо с кодом для завершщения смены E-mail адреса. Введите полученный код в поле ниже:' + }, +// + newEmailPlaceholder: { + id: 'newEmailPlaceholder', + defaultMessage: 'Enter new E-mail' + // defaultMessage: 'Введите новый E-mail' + }, + codePlaceholder: { + id: 'codePlaceholder', + defaultMessage: 'Paste the code here' + // defaultMessage: 'Вставьте код сюда' + }, + sendEmailButton: { + id: 'sendEmailButton', + defaultMessage: 'Send E-mail' + // defaultMessage: 'Отправить E-mail' + }, + changeEmailButton: { + id: 'changeEmailButton', + defaultMessage: 'Change E-mail' + // defaultMessage: 'Сменить E-mail' + }, + alreadyReceivedCode: { + id: 'alreadyReceivedCode', + defaultMessage: 'Already received code' + // defaultMessage: 'Я получил код' + } +}); diff --git a/src/components/profile/changeEmail/changeEmail.scss b/src/components/profile/changeEmail/changeEmail.scss new file mode 100644 index 0000000..54b3b82 --- /dev/null +++ b/src/components/profile/changeEmail/changeEmail.scss @@ -0,0 +1,80 @@ +@import '~components/ui/colors.scss'; + +.steps { + width: 35%; + margin: 0 auto; + height: 40px; + display: flex; + align-items: center; +} + +.step { + position: relative; + text-align: right; + width: 100%; + + height: 4px; + background: #d8d5ce; + + + &:first-child { + width: 12px; + } + + &:before { + content: ''; + display: block; + + position: absolute; + height: 4px; + left: 0; + right: 100%; + top: 50%; + margin-top: -2px; + + background: #aaa; + transition: 0.4s ease 0.1s; + } + + &:after { + content: ''; + display: inline-block; + position: relative; + top: -7px; + z-index: 1; + + width: 12px; + height: 12px; + border-radius: 100%; + + background: #aaa; + transition: background 0.4s ease; + } +} + +.activeStep { + &:before { + right: 0; + transition-delay: 0; + } + + &:after { + background: $violet; + transition-delay: 0.3s; + } +} + +.currentAccountEmail { + text-align: center; +} + + +.stepForms { + white-space: nowrap; +} + +.stepForm { + display: inline-block; + white-space: normal; + vertical-align: top; +} diff --git a/src/components/profile/changeUsername/ChangeUsername.jsx b/src/components/profile/changeUsername/ChangeUsername.jsx index f935b45..f6bc3ab 100644 --- a/src/components/profile/changeUsername/ChangeUsername.jsx +++ b/src/components/profile/changeUsername/ChangeUsername.jsx @@ -5,8 +5,8 @@ import { Link } from 'react-router'; import Helmet from 'react-helmet'; import { Input, Button, Form, FormModel } from 'components/ui/form'; - import styles from 'components/profile/profileForm.scss'; + import messages from './ChangeUsername.messages'; export default class ChangeUsername extends Component { diff --git a/src/components/profile/profileForm.scss b/src/components/profile/profileForm.scss index 00f3837..cae6aad 100644 --- a/src/components/profile/profileForm.scss +++ b/src/components/profile/profileForm.scss @@ -61,6 +61,14 @@ } } +.violetTitle { + composes: title; + + &:after { + background: $violet; + } +} + .description { font-size: 12px; color: #666666; diff --git a/src/components/ui/buttons.scss b/src/components/ui/buttons.scss index 40d21fb..7f4b925 100644 --- a/src/components/ui/buttons.scss +++ b/src/components/ui/buttons.scss @@ -71,6 +71,7 @@ @include button-theme('orange', $orange); @include button-theme('darkBlue', $dark_blue); @include button-theme('lightViolet', $light_violet); +@include button-theme('violet', $violet); .block { display: block; diff --git a/src/components/ui/form/Button.jsx b/src/components/ui/form/Button.jsx index 40d44a3..dbbf0c9 100644 --- a/src/components/ui/form/Button.jsx +++ b/src/components/ui/form/Button.jsx @@ -17,7 +17,7 @@ export default class Button extends FormComponent { PropTypes.string ]).isRequired, block: PropTypes.bool, - color: PropTypes.oneOf(['green', 'blue', 'red', 'lightViolet', 'darkBlue']) + color: PropTypes.oneOf(['green', 'blue', 'red', 'lightViolet', 'darkBlue', 'violet']) }; render() { diff --git a/src/components/ui/form/FormModel.js b/src/components/ui/form/FormModel.js index 8801408..f2b46f4 100644 --- a/src/components/ui/form/FormModel.js +++ b/src/components/ui/form/FormModel.js @@ -15,6 +15,8 @@ export default class FormModel { * @return {Object} ref and name props for component */ bindField(name) { + this.fields[name] = {}; + const props = { name, ref: (el) => { @@ -42,11 +44,17 @@ export default class FormModel { } value(fieldId) { - if (!this.fields[fieldId]) { + const field = this.fields[fieldId]; + + if (!field) { throw new Error(`The field with an id ${fieldId} does not exists`); } - return this.fields[fieldId].getValue(); + if (!field.getValue) { + return ''; // the field was not initialized through ref yet + } + + return field.getValue(); } setErrors(errors) { diff --git a/src/components/ui/form/form.scss b/src/components/ui/form/form.scss index bb7c5f3..54c5fc4 100644 --- a/src/components/ui/form/form.scss +++ b/src/components/ui/form/form.scss @@ -66,11 +66,6 @@ transition: border-color .25s; - &::placeholder { - opacity: 1; - color: #444; - } - &:hover { &, ~ .textFieldIcon { @@ -107,6 +102,11 @@ .darkTextField { background: $black; + &::placeholder { + opacity: 1; + color: #444; + } + &, ~ .textFieldIcon { border-color: lighter($black); @@ -116,6 +116,11 @@ .lightTextField { background: #fff; + &::placeholder { + opacity: 1; + color: #aaa; + } + &, ~ .textFieldIcon { border-color: #dcd8cd; diff --git a/src/pages/profile/ChangeUsernamePage.jsx b/src/pages/profile/ChangeUsernamePage.jsx index 01a0b25..da2f046 100644 --- a/src/pages/profile/ChangeUsernamePage.jsx +++ b/src/pages/profile/ChangeUsernamePage.jsx @@ -42,7 +42,6 @@ class ChangeUsernamePage extends Component { onSubmit = () => { this.props.changeUsername(this.form).then(() => { - console.log('update to', this.props.username) this.setState({ actualUsername: this.props.username }); diff --git a/src/pages/profile/ProfileChangeEmailPage.jsx b/src/pages/profile/ProfileChangeEmailPage.jsx new file mode 100644 index 0000000..a1c0f8c --- /dev/null +++ b/src/pages/profile/ProfileChangeEmailPage.jsx @@ -0,0 +1,104 @@ +import React, { Component, PropTypes } from 'react'; + +import accounts from 'services/api/accounts'; +import { FormModel } from 'components/ui/form'; +import ChangeEmail from 'components/profile/changeEmail/ChangeEmail'; +import PasswordRequestForm from 'components/profile/passwordRequestForm/PasswordRequestForm'; + +class ProfileChangeEmailPage extends Component { + static displayName = 'ProfileChangeEmailPage'; + + static propTypes = { + email: PropTypes.string.isRequired, + updateUsername: PropTypes.func.isRequired, // updates username in state + changeUsername: PropTypes.func.isRequired // saves username to backend + }; + + form = new FormModel(); + + render() { + return ( + + ); + } + + onUsernameChange = (username) => { + this.props.updateUsername(username); + }; + + onSubmit = () => { + this.props.changeUsername(this.form).then(() => { + this.setState({ + actualUsername: this.props.username + }); + }); + }; +} + +import { connect } from 'react-redux'; +import { routeActions } from 'react-router-redux'; +import { register as registerPopup, create as createPopup } from 'components/ui/popup/actions'; +import { updateUser } from 'components/user/actions'; + +function goToProfile() { + return routeActions.push('/'); +} + +export default connect((state) => ({ + email: state.user.email +}), { + updateUsername: (username) => { + return updateUser({username}); + }, + changeUsername: (form) => { + return (dispatch) => accounts.changeUsername(form.serialize()) + .catch((resp) => { + // prevalidate user input, because requestPassword popup will block the + // entire form from input, so it must be valid + if (resp.errors) { + Reflect.deleteProperty(resp.errors, 'password'); + + if (Object.keys(resp.errors).length) { + form.setErrors(resp.errors); + return Promise.reject(resp); + } + } + + return Promise.resolve(); + }) + .then(() => { + return new Promise((resolve) => { + // TODO: судя по всему registerPopup было явно лишним. Надо еще раз + // обдумать API и переписать + dispatch(registerPopup('requestPassword', PasswordRequestForm)); + dispatch(createPopup('requestPassword', (props) => ({ + form, + onSubmit: () => { + // TODO: hide this logic in action + accounts.changeUsername(form.serialize()) + .catch((resp) => { + if (resp.errors) { + form.setErrors(resp.errors); + } + + return Promise.reject(resp); + }) + .then(() => { + dispatch(updateUser({ + username: form.value('username') + })); + }) + .then(resolve) + .then(props.onClose) + .then(() => dispatch(goToProfile())); + } + }))); + }); + }) + ; + } +})(ProfileChangeEmailPage); diff --git a/src/routes.js b/src/routes.js index 5973916..ea7f7a0 100644 --- a/src/routes.js +++ b/src/routes.js @@ -8,6 +8,7 @@ import AuthPage from 'pages/auth/AuthPage'; import ProfilePage from 'pages/profile/ProfilePage'; import ProfileChangePasswordPage from 'pages/profile/ChangePasswordPage'; import ProfileChangeUsernamePage from 'pages/profile/ChangeUsernamePage'; +import ProfileChangeEmailPage from 'pages/profile/ProfileChangeEmailPage'; import { authenticate } from 'components/user/actions'; @@ -57,6 +58,7 @@ export default function routesFactory(store) { + );