From a94ddaf13130428df3f25912cd6f02a1a7f2a348 Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Sat, 13 Feb 2016 17:28:47 +0200 Subject: [PATCH] =?UTF-8?q?=D0=92=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=BC?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D0=B1=D0=BB=D0=B8=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B7=D0=B0=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BB=D1=81=D1=8F=20=D1=81=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=BA=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/auth/Activation.jsx | 105 +++++------- src/components/auth/AuthError.jsx | 76 +++++++++ src/components/auth/AuthError.messages.js | 83 ++++++++++ src/components/auth/BaseAuthBody.jsx | 43 +++++ src/components/auth/Login.jsx | 96 ++++------- src/components/auth/Logout.jsx | 25 +++ src/components/auth/PanelTransition.jsx | 146 ++++++++++------ src/components/auth/Password.jsx | 154 +++++++---------- src/components/auth/Permissions.jsx | 103 ++++-------- src/components/auth/Register.jsx | 153 +++++++++-------- src/components/auth/actions.js | 104 ++++++++++++ src/components/auth/reducer.js | 23 +++ src/components/ui/Form.jsx | 184 ++++++++++++++++----- src/components/ui/Panel.jsx | 48 ++++-- src/components/ui/buttons.scss | 1 + src/components/ui/form.scss | 46 +++++- src/components/ui/panel.scss | 11 ++ src/components/user/User.js | 58 +++++++ src/components/user/actions.js | 23 +++ src/components/user/reducer.js | 25 +++ src/components/userbar/Userbar.jsx | 27 ++- src/components/userbar/Userbar.messages.js | 5 + src/index.js | 4 +- src/pages/auth/AuthPage.jsx | 7 +- src/pages/auth/auth.scss | 2 +- src/pages/root/RootPage.jsx | 16 +- src/reducers.js | 5 + src/routes.js | 63 ++++--- src/services/request.js | 27 +++ 29 files changed, 1171 insertions(+), 492 deletions(-) create mode 100644 src/components/auth/AuthError.jsx create mode 100644 src/components/auth/AuthError.messages.js create mode 100644 src/components/auth/BaseAuthBody.jsx create mode 100644 src/components/auth/Logout.jsx create mode 100644 src/components/auth/actions.js create mode 100644 src/components/auth/reducer.js create mode 100644 src/components/user/User.js create mode 100644 src/components/user/actions.js create mode 100644 src/components/user/reducer.js create mode 100644 src/services/request.js diff --git a/src/components/auth/Activation.jsx b/src/components/auth/Activation.jsx index 3c6e5d2..4804e9c 100644 --- a/src/components/auth/Activation.jsx +++ b/src/components/auth/Activation.jsx @@ -1,48 +1,69 @@ -import React, { Component } from 'react'; +import React, { PropTypes } from 'react'; import { FormattedMessage as Message } from 'react-intl'; import Helmet from 'react-helmet'; import buttons from 'components/ui/buttons.scss'; -import { Panel, PanelBody, PanelFooter } from 'components/ui/Panel'; import { Input } from 'components/ui/Form'; +import BaseAuthBody from './BaseAuthBody'; import styles from './activation.scss'; -import {helpLinks as helpLinksStyles} from './helpLinks.scss'; import messages from './Activation.messages'; -export default function Activation() { - var Title = () => ( // TODO: separate component for PageTitle - - {(msg) => {msg}} - - ); - Title.goBack = '/register'; +class Body extends BaseAuthBody { + static propTypes = { + ...BaseAuthBody.propTypes, + activate: PropTypes.func.isRequired, + auth: PropTypes.shape({ + error: PropTypes.string, + login: PropTypes.shape({ + login: PropTypes.stirng + }) + }) + }; - return { - Title, - Body: () => ( + render() { + return (
+ {this.renderErrors()} +
erickskrauch@yandex.ru) + email: ({this.props.user.email}) }} />
- +
- ), - Footer: (props) => ( - ), @@ -53,45 +74,3 @@ export default function Activation() { ) }; } - - -export class _Activation extends Component { - displayName = 'Activation'; - - render() { - return ( -
- - {(msg) => } - - - }> - -
-
- -
- erickskrauch@yandex.ru) - }} /> -
-
-
- -
- - - - - -
- - - -
-
- ); - } -} diff --git a/src/components/auth/AuthError.jsx b/src/components/auth/AuthError.jsx new file mode 100644 index 0000000..a8f7873 --- /dev/null +++ b/src/components/auth/AuthError.jsx @@ -0,0 +1,76 @@ +import React, { Component, PropTypes } from 'react'; + +import { FormattedMessage as Message } from 'react-intl'; +import { PanelBodyHeader } from 'components/ui/Panel'; + +import messages from './AuthError.messages'; + +export default class AuthError extends Component { + static displayName = 'AuthError'; + + static propTypes = { + error: PropTypes.string.isRequired, + onClose: PropTypes.func + }; + + render() { + let { error } = this.props; + + if (this.errorsMap[error]) { + error = this.errorsMap[error](); + } + + return ( + + {error} + + ); + } + + errorsMap = { + 'error.login_required': () => , + 'error.login_not_exist': () => , + 'error.password_required': () => , + + 'error.password_incorrect': () => ( + + +
+ + + + ) + }} /> +
+ ), + + 'error.username_required': () => , + 'error.email_required': () => , + 'error.email_invalid': () => , + + 'error.email_not_available': () => ( + + +
+ + + + ) + }} /> +
+ ), + + 'error.rePassword_required': () => , + 'error.password_too_short': () => , + 'error.rePassword_does_not_match': () => , + 'error.rulesAgreement_required': () => , + 'error.you_must_accept_rules': () => this.errorsMap['error.rulesAgreement_required'](), + 'error.key_required': () => , + 'error.key_is_required': () => this.errorsMap['error.key_required'](), + 'error.key_not_exists': () => + }; +} diff --git a/src/components/auth/AuthError.messages.js b/src/components/auth/AuthError.messages.js new file mode 100644 index 0000000..bd0c397 --- /dev/null +++ b/src/components/auth/AuthError.messages.js @@ -0,0 +1,83 @@ +import { defineMessages } from 'react-intl'; + +export default defineMessages({ + invalidPassword: { + id: 'invalidPassword', + defaultMessage: 'You entered wrong account password.' + }, + + suggestResetPassword: { + id: 'suggestResetPassword', + defaultMessage: 'Are you have {link}?' + }, + + forgotYourPassword: { + id: 'forgotYourPassword', + defaultMessage: 'forgot your password' + }, + + loginRequired: { + id: 'loginRequired', + defaultMessage: 'Please enter email or username' + }, + + loginNotExist: { + id: 'loginNotExist', + defaultMessage: 'Sorry, Ely doesn\'t recognise your login.' + }, + + passwordRequired: { + id: 'passwordRequired', + defaultMessage: 'Please enter password' + }, + + usernameRequired: { + id: 'usernameRequired', + defaultMessage: 'Username is required' + }, + + emailRequired: { + id: 'emailRequired', + defaultMessage: 'Email is required' + }, + + emailInvalid: { + id: 'emailInvalid', + defaultMessage: 'Email is invalid' + }, + + emailNotAvailable: { + id: 'emailNotAvailable', + defaultMessage: 'This email is already registered.' + }, + + rePasswordRequired: { + id: 'rePasswordRequired', + defaultMessage: 'Please retype your password' + }, + + passwordTooShort: { + id: 'passwordTooShort', + defaultMessage: 'Your password is too short' + }, + + passwordsDoesNotMatch: { + id: 'passwordsDoesNotMatch', + defaultMessage: 'The passwords does not match' + }, + + rulesAgreementRequired: { + id: 'rulesAgreementRequired', + defaultMessage: 'You must accept rules in order to create an account' + }, + + keyRequired: { + id: 'keyRequired', + defaultMessage: 'Please, enter an activation key' + }, + + keyNotExists: { + id: 'keyNotExists', + defaultMessage: 'The key is incorrect' + } +}); diff --git a/src/components/auth/BaseAuthBody.jsx b/src/components/auth/BaseAuthBody.jsx new file mode 100644 index 0000000..295f49a --- /dev/null +++ b/src/components/auth/BaseAuthBody.jsx @@ -0,0 +1,43 @@ +/** + * Helps with form fields binding, form serialization and errors rendering + */ +import React, { Component, PropTypes } from 'react'; + +import AuthError from './AuthError'; + +export default class BaseAuthBody extends Component { + static propTypes = { + clearErrors: PropTypes.func.isRequired, + auth: PropTypes.shape({ + error: PropTypes.string + }) + }; + + renderErrors() { + return this.props.auth.error + ? + : '' + ; + } + + onClearErrors = this.props.clearErrors; + + form = {}; + + bindField(name) { + return { + name, + ref: (el) => { + this.form[name] = el; + } + }; + } + + serialize() { + return Object.keys(this.form).reduce((acc, key) => { + acc[key] = this.form[key].getValue(); + + return acc; + }, {}); + } +} diff --git a/src/components/auth/Login.jsx b/src/components/auth/Login.jsx index bd0a9ef..e62fa88 100644 --- a/src/components/auth/Login.jsx +++ b/src/components/auth/Login.jsx @@ -1,40 +1,57 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { routeActions } from 'react-router-redux'; +import React, { PropTypes } from 'react'; import { FormattedMessage as Message } from 'react-intl'; import Helmet from 'react-helmet'; import buttons from 'components/ui/buttons.scss'; -import { Panel, PanelBody, PanelFooter } from 'components/ui/Panel'; import { Input } from 'components/ui/Form'; +import BaseAuthBody from './BaseAuthBody'; import messages from './Login.messages'; -import {helpLinks as helpLinksStyles} from './helpLinks.scss'; import passwordMessages from './Password.messages'; -export default function Login() { - var context = { - onSubmit(event) { - event.preventDefault(); - - this.props.push('/password'); - } +class Body extends BaseAuthBody { + static propTypes = { + ...BaseAuthBody.propTypes, + login: PropTypes.func.isRequired, + auth: PropTypes.shape({ + error: PropTypes.string, + login: PropTypes.shape({ + login: PropTypes.stirng + }) + }) }; + render() { + return ( +
+ {this.renderErrors()} + + +
+ ); + } + + onFormSubmit() { + this.props.login(this.serialize()); + } +} + +export default function Login() { return { Title: () => ( // TODO: separate component for PageTitle {(msg) => {msg}} ), - Body: () => , - Footer: (props) => ( - ), @@ -45,44 +62,3 @@ export default function Login() { ) }; } - -class _Login extends Component { - displayName = 'Login'; - - render() { - return ( -
- - {(msg) => } - - - }> - - - - - - - -
- - - -
-
- ); - } - - onSubmit = (event) => { - event.preventDefault(); - - this.props.push('/password'); - }; -} - - -// export connect(null, { -// push: routeActions.push -// })(Login); diff --git a/src/components/auth/Logout.jsx b/src/components/auth/Logout.jsx new file mode 100644 index 0000000..6d99ec8 --- /dev/null +++ b/src/components/auth/Logout.jsx @@ -0,0 +1,25 @@ +import React, { Component, PropTypes } from 'react'; + +import { connect } from 'react-redux'; + +import { logout } from 'components/auth/actions'; + +class Logout extends Component { + static displayName = 'Logout'; + + static propTypes = { + logout: PropTypes.func.isRequired + }; + + componentWillMount() { + this.props.logout(); + } + + render() { + return ; + } +} + +export default connect(null, { + logout +})(Logout); diff --git a/src/components/auth/PanelTransition.jsx b/src/components/auth/PanelTransition.jsx index 8f5a44d..6ac854b 100644 --- a/src/components/auth/PanelTransition.jsx +++ b/src/components/auth/PanelTransition.jsx @@ -1,48 +1,73 @@ -import React, { Component } from 'react'; +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { routeActions } from 'react-router-redux'; import { TransitionMotion, spring } from 'react-motion'; import ReactHeight from 'react-height'; import { Panel, PanelBody, PanelFooter, PanelHeader } from 'components/ui/Panel'; +import { Form } from 'components/ui/Form'; import {helpLinks as helpLinksStyles} from 'components/auth/helpLinks.scss'; import panelStyles from 'components/ui/panel.scss'; import icons from 'components/ui/icons.scss'; +import * as actions from './actions'; const opacitySpringConfig = [300, 20]; const transformSpringConfig = [500, 50]; const changeContextSpringConfig = [500, 20]; -export default class PanelTransition extends Component { +class PanelTransition extends Component { + static displayName = 'PanelTransition'; + + static propTypes = { + auth: PropTypes.shape({ + error: PropTypes.string, + login: PropTypes.shape({ + login: PropTypes.string, + password: PropTypes.string + }) + }).isRequired, + goBack: React.PropTypes.func.isRequired, + setError: React.PropTypes.func.isRequired, + clearErrors: React.PropTypes.func.isRequired, + path: PropTypes.string.isRequired, + Title: PropTypes.element.isRequired, + Body: PropTypes.element.isRequired, + Footer: PropTypes.element.isRequired, + Links: PropTypes.element.isRequired + }; + state = { height: {}, contextHeight: 0 }; componentWillReceiveProps(nextProps) { - var previousRoute = this.props.location; + var nextPath = nextProps.path; + var previousPath = this.props.path; - var next = nextProps.path; - var prev = previousRoute && previousRoute.pathname; + if (nextPath !== previousPath) { + var direction = this.getDirection(nextPath, previousPath); + var forceHeight = direction === 'Y' && nextPath !== previousPath ? 1 : 0; - var direction = this.getDirection(next, next, prev); - var forceHeight = direction === 'Y' && next !== prev ? 1 : 0; + this.props.clearErrors(); + this.setState({ + direction, + forceHeight, + previousPath + }); - this.setState({ - direction, - forceHeight, - previousRoute - }); - - if (forceHeight) { - setTimeout(() => { - this.setState({forceHeight: 0}); - }, 100); + if (forceHeight) { + setTimeout(() => { + this.setState({forceHeight: 0}); + }, 100); + } } } render() { - var {previousRoute, height, contextHeight, forceHeight} = this.state; + const {height, canAnimateHeight, contextHeight, forceHeight} = this.state; const {path, Title, Body, Footer, Links} = this.props; @@ -54,7 +79,7 @@ export default class PanelTransition extends Component { Body, Footer, Links, - hasBackButton: previousRoute && previousRoute.pathname === Title.type.goBack, + hasBackButton: Title.type.goBack, transformSpring: spring(0, transformSpringConfig), opacitySpring: spring(1, opacitySpringConfig) }, @@ -67,24 +92,28 @@ export default class PanelTransition extends Component { willLeave={this.willLeave} > {(items) => { - var keys = Object.keys(items).filter((key) => key !== 'common'); + const keys = Object.keys(items).filter((key) => key !== 'common'); + + const contentHeight = { + overflow: 'hidden', + height: forceHeight ? items.common.switchContextHeightSpring : 'auto' + }; + + const bodyHeight = { + position: 'relative', + height: `${canAnimateHeight ? items.common.heightSpring : height[path]}px` + }; return ( -
+
{keys.map((key) => this.getHeader(key, items[key]))} -
- +
+ -
+
{keys.map((key) => this.getBody(key, items[key]))}
@@ -97,21 +126,24 @@ export default class PanelTransition extends Component {
{keys.map((key) => this.getLinks(key, items[key]))}
-
+ ); }} ); } - willEnter = (key, styles) => { - return this.getTransitionStyles(key, styles); + onFormSubmit = () => { + this.body.onFormSubmit(); }; - willLeave = (key, styles) => { - return this.getTransitionStyles(key, styles, {isLeave: true}); + onFormInvalid = (errorMessage) => { + this.props.setError(errorMessage); }; + willEnter = (key, styles) => this.getTransitionStyles(key, styles); + willLeave = (key, styles) => this.getTransitionStyles(key, styles, {isLeave: true}); + /** * @param {string} key * @param {Object} styles @@ -140,8 +172,11 @@ export default class PanelTransition extends Component { }; } - updateHeight = (height) => { + onUpdateHeight = (height) => { + const canAnimateHeight = Object.keys(this.state.height).length > 1 || this.state.height[[this.props.path]]; + this.setState({ + canAnimateHeight, height: { ...this.state.height, [this.props.path]: height @@ -149,7 +184,7 @@ export default class PanelTransition extends Component { }); }; - updateContextHeight = (height) => { + onUpdateContextHeight = (height) => { this.setState({ contextHeight: height }); @@ -158,10 +193,11 @@ export default class PanelTransition extends Component { onGoBack = (event) => { event.preventDefault(); - this.props.history.goBack(); + this.body.onGoBack && this.body.onGoBack(); + this.props.goBack(); }; - getDirection(key, next, prev) { + getDirection(next, prev) { var not = (path) => prev !== path && next !== path; var map = { @@ -172,7 +208,7 @@ export default class PanelTransition extends Component { '/oauth/permissions': 'Y' }; - return map[key]; + return map[next]; } getHeader(key, props) { @@ -192,7 +228,7 @@ export default class PanelTransition extends Component { }; var backButton = ( - ); @@ -201,7 +237,7 @@ export default class PanelTransition extends Component {
{hasBackButton ? backButton : null}
- {Title} + {React.cloneElement(Title, this.props)}
); @@ -228,8 +264,13 @@ export default class PanelTransition extends Component { }; return ( - - {Body} + + {React.cloneElement(Body, { + ...this.props, + ref: (body) => { + this.body = body; + } + })} ); } @@ -241,7 +282,7 @@ export default class PanelTransition extends Component { return (
- {Footer} + {React.cloneElement(Footer, this.props)}
); } @@ -253,7 +294,7 @@ export default class PanelTransition extends Component { return (
- {Links} + {React.cloneElement(Links, this.props)}
); } @@ -293,3 +334,16 @@ export default class PanelTransition extends Component { } } +export default connect((state) => ({ + user: state.user, + auth: state.auth, + path: state.routing.location.pathname +}), { + goBack: routeActions.goBack, + login: actions.login, + logout: actions.logout, + register: actions.register, + activate: actions.activate, + clearErrors: actions.clearErrors, + setError: actions.setError +})(PanelTransition); diff --git a/src/components/auth/Password.jsx b/src/components/auth/Password.jsx index 64a10ab..a9352e5 100644 --- a/src/components/auth/Password.jsx +++ b/src/components/auth/Password.jsx @@ -1,61 +1,86 @@ -import React, { Component } from 'react'; +import React, { PropTypes } from 'react'; import { FormattedMessage as Message } from 'react-intl'; import Helmet from 'react-helmet'; import buttons from 'components/ui/buttons.scss'; import icons from 'components/ui/icons.scss'; -import { Panel, PanelBody, PanelFooter, PanelBodyHeader } from 'components/ui/Panel'; import { Input, Checkbox } from 'components/ui/Form'; +import BaseAuthBody from './BaseAuthBody'; import styles from './password.scss'; -import {helpLinks as helpLinksStyles} from './helpLinks.scss'; import messages from './Password.messages'; +class Body extends BaseAuthBody { + static propTypes = { + ...BaseAuthBody.propTypes, + login: PropTypes.func.isRequired, + logout: PropTypes.func.isRequired, + auth: PropTypes.shape({ + error: PropTypes.string, + login: PropTypes.shape({ + login: PropTypes.stirng, + password: PropTypes.stirng + }) + }) + }; + + render() { + const {user} = this.props; + + return ( +
+ {this.renderErrors()} + +
+
+ {user.avatar + ? + : + } +
+
+ {user.email || user.username} +
+
+ + + } /> +
+ ); + } + + onFormSubmit() { + this.props.login({ + ...this.serialize(), + login: this.props.user.email || this.props.user.username + }); + } + + onGoBack() { + this.props.logout(); + } +} + export default function Password() { var Title = () => ( // TODO: separate component for PageTitle {(msg) => {msg}} ); - Title.goBack = '/login'; + Title.goBack = true; return { Title, - Body: () => ( -
- - -
- - - - ) - }} /> -
-
-
- {/**/} - -
-
- {/* На деле тут может быть и ник, в зависимости от того, что введут в 1 вьюху */} - erickskrauch@yandex.ru -
-
- - - } /> -
- ), - Footer: (props) => ( - ), @@ -66,56 +91,3 @@ export default function Password() { ) }; } - -export class _Password extends Component { - displayName = 'Password'; - - render() { - return ( -
- - {(msg) => } - - - }> - - - -
- - - - ) - }} /> -
-
-
- {/**/} - -
-
- {/* На деле тут может быть и ник, в зависимости от того, что введут в 1 вьюху */} - erickskrauch@yandex.ru -
-
- - - } /> -
- - - -
-
- - - -
-
- ); - } -} diff --git a/src/components/auth/Permissions.jsx b/src/components/auth/Permissions.jsx index f12bf9b..102b782 100644 --- a/src/components/auth/Permissions.jsx +++ b/src/components/auth/Permissions.jsx @@ -1,25 +1,33 @@ -import React, { Component } from 'react'; +import React, { PropTypes } from 'react'; import { FormattedMessage as Message } from 'react-intl'; import Helmet from 'react-helmet'; import buttons from 'components/ui/buttons.scss'; import icons from 'components/ui/icons.scss'; -import { Panel, PanelBody, PanelFooter, PanelBodyHeader } from 'components/ui/Panel'; +import { PanelBodyHeader } from 'components/ui/Panel'; +import BaseAuthBody from './BaseAuthBody'; import styles from './permissions.scss'; -import {helpLinks as helpLinksStyles} from './helpLinks.scss'; import messages from './Permissions.messages'; -export default function Permissions() { - return { - Title: () => ( // TODO: separate component for PageTitle - - {(msg) => {msg}} - - ), - Body: () => ( +class Body extends BaseAuthBody { + static propTypes = { + ...BaseAuthBody.propTypes, + login: PropTypes.func.isRequired, + auth: PropTypes.shape({ + error: PropTypes.string, + login: PropTypes.shape({ + login: PropTypes.stirng + }) + }) + }; + + render() { + return (
+ {this.renderErrors()} +
@@ -30,7 +38,7 @@ export default function Permissions() {
- erickskrauch@yandex.ru + {'erickskrauch@yandex.ru'}
@@ -47,9 +55,24 @@ export default function Permissions() {
+ ); + } + + onFormSubmit() { + // TODO + } +} + +export default function Permissions() { + return { + Title: () => ( // TODO: separate component for PageTitle + + {(msg) => {msg}} + ), + Body, Footer: () => ( - ), @@ -60,57 +83,3 @@ export default function Permissions() { ) }; } - -export class _Permissions extends Component { - displayName = 'Permissions'; - - render() { - return ( -
- - {(msg) => } - - - }> - - -
-
- {/**/} - -
-
- -
-
- erickskrauch@yandex.ru -
-
-
-
-
- -
-
    -
  • Authorization for Minecraft servers
  • -
  • Manage your skins directory and additional rows for multiline
  • -
  • Change the active skin
  • -
  • View your E-mail address
  • -
-
-
- - - -
-
- - - -
-
- ); - } -} diff --git a/src/components/auth/Register.jsx b/src/components/auth/Register.jsx index fb4fa0c..9dd32b2 100644 --- a/src/components/auth/Register.jsx +++ b/src/components/auth/Register.jsx @@ -1,16 +1,93 @@ -import React, { Component } from 'react'; +import React, { PropTypes } from 'react'; import { FormattedMessage as Message } from 'react-intl'; import Helmet from 'react-helmet'; import buttons from 'components/ui/buttons.scss'; -import { Panel, PanelBody, PanelFooter } from 'components/ui/Panel'; import { Input, Checkbox } from 'components/ui/Form'; -import {helpLinks as helpLinksStyles} from './helpLinks.scss'; +import BaseAuthBody from './BaseAuthBody'; import messages from './Register.messages'; import activationMessages from './Activation.messages'; +// TODO: password and username can be validate for length and sameness + +class Body extends BaseAuthBody { + static propTypes = { + ...BaseAuthBody.propTypes, + register: PropTypes.func.isRequired, + auth: PropTypes.shape({ + error: PropTypes.string, + register: PropTypes.shape({ + email: PropTypes.string, + username: PropTypes.stirng, + password: PropTypes.stirng, + rePassword: PropTypes.stirng, + rulesAgreement: PropTypes.boolean + }) + }) + }; + + render() { + return ( +
+ {this.renderErrors()} + + + + + + + + + + + + + ) + }} /> + } + /> +
+ ); + } + + onFormSubmit() { + this.props.register(this.serialize()); + } +} + export default function Register() { return { Title: () => ( // TODO: separate component for PageTitle @@ -18,30 +95,9 @@ export default function Register() { {(msg) => {msg}} ), - Body: () => ( -
- - - - - - - - - ) - }} /> - } /> -
- ), - Footer: (props) => ( - ), @@ -52,46 +108,3 @@ export default function Register() { ) }; } - -export class _Register extends Component { - displayName = 'Register'; - - render() { - return ( -
- - {(msg) => } - - - }> - - - - - - - - - - ) - }} /> - } /> - - - - - -
- - - -
-
- ); - } -} diff --git a/src/components/auth/actions.js b/src/components/auth/actions.js new file mode 100644 index 0000000..d79c008 --- /dev/null +++ b/src/components/auth/actions.js @@ -0,0 +1,104 @@ +import { routeActions } from 'react-router-redux'; + +import { updateUser, logout as logoutUser } from 'components/user/actions'; +import request from 'services/request'; + +export function login({login = '', password = '', rememberMe = false}) { + const PASSWORD_REQUIRED = 'error.password_required'; + const LOGIN_REQUIRED = 'error.login_required'; + + return (dispatch) => + request.post( + '/api/authentication/login', + {login, password, rememberMe} + ) + .then(() => { + dispatch(updateUser({ + isGuest: false + })); + + dispatch(redirectToGoal()); + }) + .catch((resp) => { + if (resp.errors.password === PASSWORD_REQUIRED) { + dispatch(updateUser({ + username: login, + email: login + })); + dispatch(routeActions.push('/password')); + } else { + if (resp.errors.login === LOGIN_REQUIRED && password) { + dispatch(logout()); + } + const errorMessage = resp.errors[Object.keys(resp.errors)[0]]; + dispatch(setError(errorMessage)); + } + }) + ; +} + +export function register({ + email = '', + username = '', + password = '', + rePassword = '', + rulesAgreement = false +}) { + return (dispatch) => + request.post( + '/api/signup/register', + {email, username, password, rePassword, rulesAgreement} + ) + .then(() => { + dispatch(routeActions.push('/activation')); + }) + .catch((resp) => { + const errorMessage = resp.errors[Object.keys(resp.errors)[0]]; + dispatch(setError(errorMessage)); + }) + ; +} + +export function activate({key = ''}) { + return (dispatch) => + request.post( + '/api/signup/confirm', + {key} + ) + .then(() => { + dispatch(updateUser({ + isActive: true + })); + + dispatch(redirectToGoal()); + }) + .catch((resp) => { + const errorMessage = resp.errors[Object.keys(resp.errors)[0]]; + dispatch(setError(errorMessage)); + }) + ; +} + +function redirectToGoal() { + return routeActions.push('/oauth/permissions'); +} + +export const ERROR = 'error'; +export function setError(error) { + return { + type: ERROR, + payload: error, + error: true + }; +} + +export function clearErrors() { + return setError(null); +} + +export function logout() { + return (dispatch) => { + dispatch(logoutUser()); + dispatch(routeActions.push('/login')); + }; +} diff --git a/src/components/auth/reducer.js b/src/components/auth/reducer.js new file mode 100644 index 0000000..b6c59b1 --- /dev/null +++ b/src/components/auth/reducer.js @@ -0,0 +1,23 @@ +import { combineReducers } from 'redux'; + +import { ERROR } from './actions'; + +export default combineReducers({ + error +}); + +function error( + state = null, + {type, payload = null, error = false} +) { + switch (type) { + case ERROR: + if (!error) { + throw new Error('Expected payload with error'); + } + return payload; + + default: + return state; + } +} diff --git a/src/components/ui/Form.jsx b/src/components/ui/Form.jsx index c06435e..7dd2a25 100644 --- a/src/components/ui/Form.jsx +++ b/src/components/ui/Form.jsx @@ -1,60 +1,166 @@ -import React, { Component } from 'react'; +import React, { Component, PropTypes } from 'react'; import classNames from 'classnames'; -import {injectIntl, intlShape} from 'react-intl'; +import { intlShape } from 'react-intl'; import icons from './icons.scss'; import styles from './form.scss'; -function Input(props) { - var { icon, color = 'green' } = props; +export class Input extends Component { + static displayName = 'Input'; - props = { - type: 'text', - ...props + static propTypes = { + placeholder: PropTypes.shape({ + id: PropTypes.string + }), + icon: PropTypes.string, + color: PropTypes.oneOf(['green', 'blue', 'red']) }; - if (props.placeholder && props.placeholder.id) { - props.placeholder = props.intl.formatMessage(props.placeholder); - } + static contextTypes = { + intl: intlShape.isRequired + }; - var baseClass = styles.formRow; - if (icon) { - baseClass = styles.formIconRow; - icon = ( -
+ render() { + let { icon, color = 'green' } = this.props; + + const props = { + type: 'text', + ...this.props + }; + + if (props.placeholder && props.placeholder.id) { + props.placeholder = this.context.intl.formatMessage(props.placeholder); + } + + let baseClass = styles.formRow; + if (icon) { + baseClass = styles.formIconRow; + icon = ( +
+ ); + } + + return ( +
+ + {icon} +
); } - return ( -
- - {icon} -
- ); + setEl = (el) => { + this.el = el; + }; + + getValue() { + return this.el.value; + } } -Input.displayName = 'Input'; -Input.propTypes = { - intl: intlShape.isRequired -}; +export class Checkbox extends Component { + static displayName = 'Checkbox'; -const IntlInput = injectIntl(Input); + static propTypes = { + color: PropTypes.oneOf(['green', 'blue', 'red']) + }; -export {IntlInput as Input}; + render() { + const { label, color = 'green' } = this.props; -export function Checkbox(props) { - var { label, color = 'green' } = props; + return ( +
+