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