mirror of
				https://github.com/elyby/accounts-frontend.git
				synced 2025-05-31 14:11:58 +05:30 
			
		
		
		
	В первом приближении заинтегрировался с беком
This commit is contained in:
		| @@ -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 | ||||
|         <Message {...messages.accountActivationTitle}> | ||||
|             {(msg) => <span>{msg}<Helmet title={msg} /></span>} | ||||
|         </Message> | ||||
|     ); | ||||
|     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 ( | ||||
|             <div> | ||||
|                 {this.renderErrors()} | ||||
|  | ||||
|                 <div className={styles.description}> | ||||
|                     <div className={styles.descriptionImage} /> | ||||
|  | ||||
|                     <div className={styles.descriptionText}> | ||||
|                         <Message {...messages.activationMailWasSent} values={{ | ||||
|                             email: (<b>erickskrauch@yandex.ru</b>) | ||||
|                             email: (<b>{this.props.user.email}</b>) | ||||
|                         }} /> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div className={styles.formRow}> | ||||
|                     <Input color="blue" className={styles.activationCodeInput} placeholder={messages.enterTheCode} /> | ||||
|                     <Input {...this.bindField('key')} | ||||
|                         color="blue" | ||||
|                         className={styles.activationCodeInput} | ||||
|                         autoFocus | ||||
|                         required | ||||
|                         placeholder={messages.enterTheCode} | ||||
|                     /> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ), | ||||
|         Footer: (props) => ( | ||||
|             <button className={buttons.blue} onClick={(event) => { | ||||
|                 event.preventDefault(); | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|                 props.history.push('/oauth/permissions'); | ||||
|             }}> | ||||
|     onFormSubmit() { | ||||
|         this.props.activate(this.serialize()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function Activation() { | ||||
|     return { | ||||
|         Title: () => ( // TODO: separate component for PageTitle | ||||
|             <Message {...messages.accountActivationTitle}> | ||||
|                 {(msg) => <span>{msg}<Helmet title={msg} /></span>} | ||||
|             </Message> | ||||
|         ), | ||||
|         Body, | ||||
|         Footer: () => ( | ||||
|             <button className={buttons.blue}> | ||||
|                 <Message {...messages.confirmEmail} /> | ||||
|             </button> | ||||
|         ), | ||||
| @@ -53,45 +74,3 @@ export default function Activation() { | ||||
|         ) | ||||
|     }; | ||||
| } | ||||
|  | ||||
|  | ||||
| export class _Activation extends Component { | ||||
|     displayName = 'Activation'; | ||||
|  | ||||
|     render() { | ||||
|         return ( | ||||
|             <div> | ||||
|                 <Message {...messages.accountActivationTitle}> | ||||
|                     {(msg) => <Helmet title={msg} />} | ||||
|                 </Message> | ||||
|  | ||||
|                 <Panel icon="arrowLeft" title={<Message {...messages.accountActivationTitle} />}> | ||||
|                     <PanelBody> | ||||
|                         <div className={styles.description}> | ||||
|                             <div className={styles.descriptionImage} /> | ||||
|  | ||||
|                             <div className={styles.descriptionText}> | ||||
|                                 <Message {...messages.activationMailWasSent} values={{ | ||||
|                                     email: (<b>erickskrauch@yandex.ru</b>) | ||||
|                                 }} /> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div className={styles.formRow}> | ||||
|                             <Input color="blue" className={styles.activationCodeInput} placeholder={messages.enterTheCode} /> | ||||
|                         </div> | ||||
|                     </PanelBody> | ||||
|                     <PanelFooter> | ||||
|                         <button className={buttons.blue}> | ||||
|                             <Message {...messages.confirmEmail} /> | ||||
|                         </button> | ||||
|                     </PanelFooter> | ||||
|                 </Panel> | ||||
|                 <div className={helpLinksStyles}> | ||||
|                     <a href="#"> | ||||
|                         <Message {...messages.didNotReceivedEmail} /> | ||||
|                     </a> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										76
									
								
								src/components/auth/AuthError.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/components/auth/AuthError.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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 ( | ||||
|             <PanelBodyHeader type="error" onClose={this.props.onClose}> | ||||
|                 {error} | ||||
|             </PanelBodyHeader> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     errorsMap = { | ||||
|         'error.login_required': () => <Message {...messages.loginRequired} />, | ||||
|         'error.login_not_exist': () => <Message {...messages.loginNotExist} />, | ||||
|         'error.password_required': () => <Message {...messages.passwordRequired} />, | ||||
|  | ||||
|         'error.password_incorrect': () => ( | ||||
|             <span> | ||||
|                 <Message {...messages.invalidPassword} /> | ||||
|                 <br/> | ||||
|                 <Message {...messages.suggestResetPassword} values={{ | ||||
|                     link: ( | ||||
|                         <a href="#"> | ||||
|                             <Message {...messages.forgotYourPassword} /> | ||||
|                         </a> | ||||
|                     ) | ||||
|                 }} /> | ||||
|             </span> | ||||
|         ), | ||||
|  | ||||
|         'error.username_required': () => <Message {...messages.usernameRequired} />, | ||||
|         'error.email_required': () => <Message {...messages.emailRequired} />, | ||||
|         'error.email_invalid': () => <Message {...messages.emailInvalid} />, | ||||
|  | ||||
|         'error.email_not_available': () => ( | ||||
|             <span> | ||||
|                 <Message {...messages.emailNotAvailable} /> | ||||
|                 <br/> | ||||
|                 <Message {...messages.suggestResetPassword} values={{ | ||||
|                     link: ( | ||||
|                         <a href="#"> | ||||
|                             <Message {...messages.forgotYourPassword} /> | ||||
|                         </a> | ||||
|                     ) | ||||
|                 }} /> | ||||
|             </span> | ||||
|         ), | ||||
|  | ||||
|         'error.rePassword_required': () => <Message {...messages.rePasswordRequired} />, | ||||
|         'error.password_too_short': () => <Message {...messages.passwordTooShort} />, | ||||
|         'error.rePassword_does_not_match': () => <Message {...messages.passwordsDoesNotMatch} />, | ||||
|         'error.rulesAgreement_required': () => <Message {...messages.rulesAgreementRequired} />, | ||||
|         'error.you_must_accept_rules': () => this.errorsMap['error.rulesAgreement_required'](), | ||||
|         'error.key_required': () => <Message {...messages.keyRequired} />, | ||||
|         'error.key_is_required': () => this.errorsMap['error.key_required'](), | ||||
|         'error.key_not_exists': () => <Message {...messages.keyNotExists} /> | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										83
									
								
								src/components/auth/AuthError.messages.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/components/auth/AuthError.messages.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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' | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										43
									
								
								src/components/auth/BaseAuthBody.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/components/auth/BaseAuthBody.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|             ? <AuthError error={this.props.auth.error} onClose={this.onClearErrors} /> | ||||
|             : '' | ||||
|             ; | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|         }, {}); | ||||
|     } | ||||
| } | ||||
| @@ -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 ( | ||||
|             <div> | ||||
|                 {this.renderErrors()} | ||||
|  | ||||
|                 <Input {...this.bindField('login')} | ||||
|                     icon="envelope" | ||||
|                     autoFocus | ||||
|                     required | ||||
|                     placeholder={messages.emailOrUsername} | ||||
|                 /> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     onFormSubmit() { | ||||
|         this.props.login(this.serialize()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function Login() { | ||||
|     return { | ||||
|         Title: () => ( // TODO: separate component for PageTitle | ||||
|             <Message {...messages.loginTitle}> | ||||
|                 {(msg) => <span>{msg}<Helmet title={msg} /></span>} | ||||
|             </Message> | ||||
|         ), | ||||
|         Body: () => <Input icon="envelope" type="email" placeholder={messages.emailOrUsername} />, | ||||
|         Footer: (props) => ( | ||||
|             <button className={buttons.green} onClick={(event) => { | ||||
|                 event.preventDefault(); | ||||
|  | ||||
|                 props.history.push('/password'); | ||||
|             }}> | ||||
|         Body, | ||||
|         Footer: () => ( | ||||
|             <button className={buttons.green} type="submit"> | ||||
|                 <Message {...messages.next} /> | ||||
|             </button> | ||||
|         ), | ||||
| @@ -45,44 +62,3 @@ export default function Login() { | ||||
|         ) | ||||
|     }; | ||||
| } | ||||
|  | ||||
| class _Login extends Component { | ||||
|     displayName = 'Login'; | ||||
|  | ||||
|     render() { | ||||
|         return ( | ||||
|             <div> | ||||
|                 <Message {...messages.loginTitle}> | ||||
|                     {(msg) => <Helmet title={msg} />} | ||||
|                 </Message> | ||||
|  | ||||
|                 <Panel title={<Message {...messages.loginTitle} />}> | ||||
|                     <PanelBody> | ||||
|                         <Input icon="envelope" type="email" placeholder={messages.emailOrUsername} /> | ||||
|                     </PanelBody> | ||||
|                     <PanelFooter> | ||||
|                         <button className={buttons.green} onClick={this.onSubmit}> | ||||
|                             <Message {...messages.next} /> | ||||
|                         </button> | ||||
|                     </PanelFooter> | ||||
|                 </Panel> | ||||
|                 <div className={helpLinksStyles}> | ||||
|                     <a href="#"> | ||||
|                         <Message {...passwordMessages.forgotPassword} /> | ||||
|                     </a> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     onSubmit = (event) => { | ||||
|         event.preventDefault(); | ||||
|  | ||||
|         this.props.push('/password'); | ||||
|     }; | ||||
| } | ||||
|  | ||||
|  | ||||
| // export connect(null, { | ||||
| //     push: routeActions.push | ||||
| // })(Login); | ||||
|   | ||||
							
								
								
									
										25
									
								
								src/components/auth/Logout.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/components/auth/Logout.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <span />; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default connect(null, { | ||||
|     logout | ||||
| })(Logout); | ||||
| @@ -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 ( | ||||
|                         <div> | ||||
|                         <Form id={path} onSubmit={this.onFormSubmit} onInvalid={this.onFormInvalid}> | ||||
|                             <Panel> | ||||
|                                 <PanelHeader> | ||||
|                                     {keys.map((key) => this.getHeader(key, items[key]))} | ||||
|                                 </PanelHeader> | ||||
|                                 <div style={{ | ||||
|                                     overflow: 'hidden', | ||||
|                                     height: forceHeight ? items.common.switchContextHeightSpring : 'auto' | ||||
|                                 }}> | ||||
|                                     <ReactHeight onHeightReady={this.updateContextHeight}> | ||||
|                                 <div style={contentHeight}> | ||||
|                                     <ReactHeight onHeightReady={this.onUpdateContextHeight}> | ||||
|                                         <PanelBody> | ||||
|                                             <div style={{ | ||||
|                                                 position: 'relative', | ||||
|                                                 height: `${previousRoute ? items.common.heightSpring : height[path]}px` | ||||
|                                             }}> | ||||
|                                             <div style={bodyHeight}> | ||||
|                                                 {keys.map((key) => this.getBody(key, items[key]))} | ||||
|                                             </div> | ||||
|                                         </PanelBody> | ||||
| @@ -97,21 +126,24 @@ export default class PanelTransition extends Component { | ||||
|                             <div className={helpLinksStyles}> | ||||
|                                 {keys.map((key) => this.getLinks(key, items[key]))} | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         </Form> | ||||
|                     ); | ||||
|                 }} | ||||
|             </TransitionMotion> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     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 = ( | ||||
|             <button style={sideScrollStyle} onClick={this.onGoBack} className={panelStyles.headerControl}> | ||||
|             <button style={sideScrollStyle} type="button" onClick={this.onGoBack} className={panelStyles.headerControl}> | ||||
|                 <span className={icons.arrowLeft} /> | ||||
|             </button> | ||||
|         ); | ||||
| @@ -201,7 +237,7 @@ export default class PanelTransition extends Component { | ||||
|             <div key={`header${key}`} style={style}> | ||||
|                 {hasBackButton ? backButton : null} | ||||
|                 <div style={scrollStyle}> | ||||
|                     {Title} | ||||
|                     {React.cloneElement(Title, this.props)} | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
| @@ -228,8 +264,13 @@ export default class PanelTransition extends Component { | ||||
|         }; | ||||
|  | ||||
|         return ( | ||||
|             <ReactHeight key={`body${key}`} style={style} onHeightReady={this.updateHeight}> | ||||
|                 {Body} | ||||
|             <ReactHeight key={`body${key}`} style={style} onHeightReady={this.onUpdateHeight}> | ||||
|                 {React.cloneElement(Body, { | ||||
|                     ...this.props, | ||||
|                     ref: (body) => { | ||||
|                         this.body = body; | ||||
|                     } | ||||
|                 })} | ||||
|             </ReactHeight> | ||||
|         ); | ||||
|     } | ||||
| @@ -241,7 +282,7 @@ export default class PanelTransition extends Component { | ||||
|  | ||||
|         return ( | ||||
|             <div key={`footer${key}`} style={style}> | ||||
|                 {Footer} | ||||
|                 {React.cloneElement(Footer, this.props)} | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| @@ -253,7 +294,7 @@ export default class PanelTransition extends Component { | ||||
|  | ||||
|         return ( | ||||
|             <div key={`links${key}`} style={style}> | ||||
|                 {Links} | ||||
|                 {React.cloneElement(Links, this.props)} | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 ( | ||||
|             <div> | ||||
|                 {this.renderErrors()} | ||||
|  | ||||
|                 <div className={styles.miniProfile}> | ||||
|                     <div className={styles.avatar}> | ||||
|                         {user.avatar | ||||
|                             ? <img src={user.avatar} /> | ||||
|                             : <span className={icons.user} /> | ||||
|                         } | ||||
|                     </div> | ||||
|                     <div className={styles.email}> | ||||
|                         {user.email || user.username} | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <Input {...this.bindField('password')} | ||||
|                     icon="key" | ||||
|                     type="password" | ||||
|                     autoFocus | ||||
|                     required | ||||
|                     placeholder={messages.accountPassword} | ||||
|                 /> | ||||
|  | ||||
|                 <Checkbox {...this.bindField('rememberMe')} label={<Message {...messages.rememberMe} />} /> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     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 | ||||
|         <Message {...messages.passwordTitle}> | ||||
|             {(msg) => <span>{msg}<Helmet title={msg} /></span>} | ||||
|         </Message> | ||||
|     ); | ||||
|     Title.goBack = '/login'; | ||||
|     Title.goBack = true; | ||||
|  | ||||
|     return { | ||||
|         Title, | ||||
|         Body: () => ( | ||||
|             <div> | ||||
|                 <PanelBodyHeader type="error"> | ||||
|                     <Message {...messages.invalidPassword} /> | ||||
|                     <br/> | ||||
|                     <Message {...messages.suggestResetPassword} values={{ | ||||
|                         link: ( | ||||
|                             <a href="#"> | ||||
|                                 <Message {...messages.forgotYourPassword} /> | ||||
|                             </a> | ||||
|                         ) | ||||
|                     }} /> | ||||
|                 </PanelBodyHeader> | ||||
|                 <div className={styles.miniProfile}> | ||||
|                     <div className={styles.avatar}> | ||||
|                         {/*<img src="//lorempixel.com/g/90/90" />*/} | ||||
|                         <span className={icons.user} /> | ||||
|                     </div> | ||||
|                     <div className={styles.email}> | ||||
|                         {/* На деле тут может быть и ник, в зависимости от того, что введут в 1 вьюху */} | ||||
|                         erickskrauch@yandex.ru | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <Input icon="key" type="password" placeholder={messages.accountPassword} /> | ||||
|  | ||||
|                 <Checkbox label={<Message {...messages.rememberMe} />} /> | ||||
|             </div> | ||||
|         ), | ||||
|         Footer: (props) => ( | ||||
|             <button className={buttons.green} onClick={(event) => { | ||||
|                 event.preventDefault(); | ||||
|  | ||||
|                 props.history.push('/oauth/permissions'); | ||||
|             }}> | ||||
|         Body, | ||||
|         Footer: () => ( | ||||
|             <button className={buttons.green} type="submit"> | ||||
|                 <Message {...messages.signInButton} /> | ||||
|             </button> | ||||
|         ), | ||||
| @@ -66,56 +91,3 @@ export default function Password() { | ||||
|         ) | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export class _Password extends Component { | ||||
|     displayName = 'Password'; | ||||
|  | ||||
|     render() { | ||||
|         return ( | ||||
|             <div> | ||||
|                 <Message {...messages.passwordTitle}> | ||||
|                     {(msg) => <Helmet title={msg} />} | ||||
|                 </Message> | ||||
|  | ||||
|                 <Panel icon="arrowLeft" title={<Message {...messages.passwordTitle} />}> | ||||
|                     <PanelBody> | ||||
|                         <PanelBodyHeader type="error"> | ||||
|                             <Message {...messages.invalidPassword} /> | ||||
|                             <br/> | ||||
|                             <Message {...messages.suggestResetPassword} values={{ | ||||
|                                 link: ( | ||||
|                                     <a href="#"> | ||||
|                                         <Message {...messages.forgotYourPassword} /> | ||||
|                                     </a> | ||||
|                                 ) | ||||
|                             }} /> | ||||
|                         </PanelBodyHeader> | ||||
|                         <div className={styles.miniProfile}> | ||||
|                             <div className={styles.avatar}> | ||||
|                                 {/*<img src="//lorempixel.com/g/90/90" />*/} | ||||
|                                 <span className={icons.user} /> | ||||
|                             </div> | ||||
|                             <div className={styles.email}> | ||||
|                                 {/* На деле тут может быть и ник, в зависимости от того, что введут в 1 вьюху */} | ||||
|                                 erickskrauch@yandex.ru | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <Input icon="key" type="password" placeholder={messages.accountPassword} /> | ||||
|  | ||||
|                         <Checkbox label={<Message {...messages.rememberMe} />} /> | ||||
|                     </PanelBody> | ||||
|                     <PanelFooter> | ||||
|                         <button className={buttons.green}> | ||||
|                             <Message {...messages.signInButton} /> | ||||
|                         </button> | ||||
|                     </PanelFooter> | ||||
|                 </Panel> | ||||
|                 <div className={helpLinksStyles}> | ||||
|                     <a href="#"> | ||||
|                         <Message {...messages.forgotPassword} /> | ||||
|                     </a> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|             <Message {...messages.permissionsTitle}> | ||||
|                 {(msg) => <span>{msg}<Helmet title={msg} /></span>} | ||||
|             </Message> | ||||
|         ), | ||||
|         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 ( | ||||
|             <div> | ||||
|                 {this.renderErrors()} | ||||
|  | ||||
|                 <PanelBodyHeader> | ||||
|                     <div className={styles.authInfo}> | ||||
|                         <div className={styles.authInfoAvatar}> | ||||
| @@ -30,7 +38,7 @@ export default function Permissions() { | ||||
|                             <Message {...messages.youAuthorizedAs} /> | ||||
|                         </div> | ||||
|                         <div className={styles.authInfoEmail}> | ||||
|                             erickskrauch@yandex.ru | ||||
|                             {'erickskrauch@yandex.ru'} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </PanelBodyHeader> | ||||
| @@ -47,9 +55,24 @@ export default function Permissions() { | ||||
|                     </ul> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     onFormSubmit() { | ||||
|         // TODO | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function Permissions() { | ||||
|     return { | ||||
|         Title: () => ( // TODO: separate component for PageTitle | ||||
|             <Message {...messages.permissionsTitle}> | ||||
|                 {(msg) => <span>{msg}<Helmet title={msg} /></span>} | ||||
|             </Message> | ||||
|         ), | ||||
|         Body, | ||||
|         Footer: () => ( | ||||
|             <button className={buttons.green}> | ||||
|             <button className={buttons.orange} autoFocus> | ||||
|                 <Message {...messages.approve} /> | ||||
|             </button> | ||||
|         ), | ||||
| @@ -60,57 +83,3 @@ export default function Permissions() { | ||||
|         ) | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export class _Permissions extends Component { | ||||
|     displayName = 'Permissions'; | ||||
|  | ||||
|     render() { | ||||
|         return ( | ||||
|             <div> | ||||
|                 <Message {...messages.permissionsTitle}> | ||||
|                     {(msg) => <Helmet title={msg} />} | ||||
|                 </Message> | ||||
|  | ||||
|                 <Panel title={<Message {...messages.permissionsTitle} />}> | ||||
|                     <PanelBody> | ||||
|                         <PanelBodyHeader> | ||||
|                             <div className={styles.authInfo}> | ||||
|                                 <div className={styles.authInfoAvatar}> | ||||
|                                     {/*<img src="//lorempixel.com/g/90/90" />*/} | ||||
|                                     <span className={icons.user} /> | ||||
|                                 </div> | ||||
|                                 <div className={styles.authInfoTitle}> | ||||
|                                     <Message {...messages.youAuthorizedAs} /> | ||||
|                                 </div> | ||||
|                                 <div className={styles.authInfoEmail}> | ||||
|                                     erickskrauch@yandex.ru | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </PanelBodyHeader> | ||||
|                         <div className={styles.permissionsContainer}> | ||||
|                             <div className={styles.permissionsTitle}> | ||||
|                                 <Message {...messages.theAppNeedsAccess} /> | ||||
|                             </div> | ||||
|                             <ul className={styles.permissionsList}> | ||||
|                                 <li>Authorization for Minecraft servers</li> | ||||
|                                 <li>Manage your skins directory and additional rows for multiline</li> | ||||
|                                 <li>Change the active skin</li> | ||||
|                                 <li>View your E-mail address</li> | ||||
|                             </ul> | ||||
|                         </div> | ||||
|                     </PanelBody> | ||||
|                     <PanelFooter> | ||||
|                         <button className={buttons.green}> | ||||
|                             <Message {...messages.approve} /> | ||||
|                         </button> | ||||
|                     </PanelFooter> | ||||
|                 </Panel> | ||||
|                 <div className={helpLinksStyles}> | ||||
|                     <a href="#"> | ||||
|                         <Message {...messages.decline} /> | ||||
|                     </a> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 ( | ||||
|             <div> | ||||
|                 {this.renderErrors()} | ||||
|  | ||||
|                 <Input {...this.bindField('username')} | ||||
|                     icon="user" | ||||
|                     color="blue" | ||||
|                     type="text" | ||||
|                     autoFocus | ||||
|                     required | ||||
|                     placeholder={messages.yourNickname} | ||||
|                 /> | ||||
|  | ||||
|                 <Input {...this.bindField('email')} | ||||
|                     icon="envelope" | ||||
|                     color="blue" | ||||
|                     type="email" | ||||
|                     required | ||||
|                     placeholder={messages.yourEmail} | ||||
|                 /> | ||||
|  | ||||
|                 <Input {...this.bindField('password')} | ||||
|                     icon="key" | ||||
|                     color="blue" | ||||
|                     type="password" | ||||
|                     required | ||||
|                     placeholder={messages.accountPassword} | ||||
|                 /> | ||||
|  | ||||
|                 <Input {...this.bindField('rePassword')} | ||||
|                     icon="key" | ||||
|                     color="blue" | ||||
|                     type="password" | ||||
|                     required | ||||
|                     placeholder={messages.repeatPassword} | ||||
|                 /> | ||||
|  | ||||
|                 <Checkbox {...this.bindField('rulesAgreement')} | ||||
|                     color="blue" | ||||
|                     required | ||||
|                     label={ | ||||
|                         <Message {...messages.acceptRules} values={{ | ||||
|                             link: ( | ||||
|                                 <a href="#"> | ||||
|                                     <Message {...messages.termsOfService} /> | ||||
|                                 </a> | ||||
|                             ) | ||||
|                         }} /> | ||||
|                     } | ||||
|                 /> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     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) => <span>{msg}<Helmet title={msg} /></span>} | ||||
|             </Message> | ||||
|         ), | ||||
|         Body: () => ( | ||||
|             <div> | ||||
|                 <Input icon="user" color="blue" type="text" placeholder={messages.yourNickname} /> | ||||
|                 <Input icon="envelope" color="blue" type="email" placeholder={messages.yourEmail} /> | ||||
|                 <Input icon="key" color="blue" type="password" placeholder={messages.accountPassword} /> | ||||
|                 <Input icon="key" color="blue" type="password" placeholder={messages.repeatPassword} /> | ||||
|  | ||||
|                 <Checkbox color="blue" label={ | ||||
|                     <Message {...messages.acceptRules} values={{ | ||||
|                         link: ( | ||||
|                             <a href="#"> | ||||
|                                 <Message {...messages.termsOfService} /> | ||||
|                             </a> | ||||
|                         ) | ||||
|                     }} /> | ||||
|                 } /> | ||||
|             </div> | ||||
|         ), | ||||
|         Footer: (props) => ( | ||||
|             <button className={buttons.blue} onClick={(event) => { | ||||
|                 event.preventDefault(); | ||||
|  | ||||
|                 props.history.push('/activation'); | ||||
|             }}> | ||||
|         Body, | ||||
|         Footer: () => ( | ||||
|             <button className={buttons.blue} type="submit"> | ||||
|                 <Message {...messages.signUpButton} /> | ||||
|             </button> | ||||
|         ), | ||||
| @@ -52,46 +108,3 @@ export default function Register() { | ||||
|         ) | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export class _Register extends Component { | ||||
|     displayName = 'Register'; | ||||
|  | ||||
|     render() { | ||||
|         return ( | ||||
|             <div> | ||||
|                 <Message {...messages.registerTitle}> | ||||
|                     {(msg) => <Helmet title={msg} />} | ||||
|                 </Message> | ||||
|  | ||||
|                 <Panel title={<Message {...messages.registerTitle} />}> | ||||
|                     <PanelBody> | ||||
|                         <Input icon="user" color="blue" type="text" placeholder={messages.yourNickname} /> | ||||
|                         <Input icon="envelope" color="blue" type="email" placeholder={messages.yourEmail} /> | ||||
|                         <Input icon="key" color="blue" type="password" placeholder={messages.accountPassword} /> | ||||
|                         <Input icon="key" color="blue" type="password" placeholder={messages.repeatPassword} /> | ||||
|  | ||||
|                         <Checkbox color="blue" label={ | ||||
|                             <Message {...messages.acceptRules} values={{ | ||||
|                                 link: ( | ||||
|                                     <a href="#"> | ||||
|                                         <Message {...messages.termsOfService} /> | ||||
|                                     </a> | ||||
|                                 ) | ||||
|                             }} /> | ||||
|                         } /> | ||||
|                     </PanelBody> | ||||
|                     <PanelFooter> | ||||
|                         <button className={buttons.blue}> | ||||
|                             <Message {...messages.signUpButton} /> | ||||
|                         </button> | ||||
|                     </PanelFooter> | ||||
|                 </Panel> | ||||
|                 <div className={helpLinksStyles}> | ||||
|                     <a href="#"> | ||||
|                         <Message {...activationMessages.didNotReceivedEmail} /> | ||||
|                     </a> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										104
									
								
								src/components/auth/actions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/components/auth/actions.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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')); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/components/auth/reducer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/components/auth/reducer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -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 = ( | ||||
|             <div className={classNames(styles.formFieldIcon, icons[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 = ( | ||||
|                 <div className={classNames(styles.formFieldIcon, icons[icon])} /> | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return ( | ||||
|             <div className={baseClass}> | ||||
|                 <input ref={this.setEl} className={styles[`${color}TextField`]} {...props} /> | ||||
|                 {icon} | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <div className={baseClass}> | ||||
|             <input className={styles[`${color}TextField`]} {...props} /> | ||||
|             {icon} | ||||
|         </div> | ||||
|     ); | ||||
|     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 ( | ||||
|             <div className={styles[`${color}CheckboxRow`]}> | ||||
|                 <label className={styles.checkboxContainer}> | ||||
|                     <input ref={this.setEl} className={styles.checkboxInput} type="checkbox" {...this.props} /> | ||||
|                     <div className={styles.checkbox} /> | ||||
|                     {label} | ||||
|                 </label> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <div className={styles[`${color}CheckboxRow`]}> | ||||
|             <label className={styles.checkboxContainer}> | ||||
|                 <input className={styles.checkboxInput} type="checkbox" /> | ||||
|                 <div className={styles.checkbox} /> | ||||
|                 {label} | ||||
|             </label> | ||||
|         </div> | ||||
|     ); | ||||
|     setEl = (el) => { | ||||
|         this.el = el; | ||||
|     }; | ||||
|  | ||||
|     getValue() { | ||||
|         return this.el.checked ? 1 : 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| Checkbox.displayName = 'Checkbox'; | ||||
| export class Form extends Component { | ||||
|     static displayName = 'Form'; | ||||
|  | ||||
|     static propTypes = { | ||||
|         id: PropTypes.string, // and id, that uniquely identifies form contents | ||||
|         onSubmit: PropTypes.func, | ||||
|         onInvalid: PropTypes.func, | ||||
|         children: PropTypes.oneOfType([ | ||||
|             PropTypes.arrayOf(PropTypes.node), | ||||
|             PropTypes.node | ||||
|         ]) | ||||
|     }; | ||||
|  | ||||
|     static defaultProps = { | ||||
|         id: 'default', | ||||
|         onSubmit() {}, | ||||
|         onInvalid() {} | ||||
|     }; | ||||
|  | ||||
|     state = { | ||||
|         isTouched: false | ||||
|     }; | ||||
|  | ||||
|     componentWillReceiveProps(nextProps) { | ||||
|         if (nextProps.id !== this.props.id) { | ||||
|             this.setState({ | ||||
|                 isTouched: false | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         return ( | ||||
|             <form | ||||
|                 className={classNames( | ||||
|                     styles.form, | ||||
|                     { | ||||
|                         [styles.formTouched]: this.state.isTouched | ||||
|                     } | ||||
|                 )} | ||||
|                 onSubmit={this.onFormSubmit} | ||||
|                 noValidate | ||||
|             > | ||||
|                 {this.props.children} | ||||
|             </form> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     onFormSubmit = (event) => { | ||||
|         event.preventDefault(); | ||||
|  | ||||
|         if (!this.state.isTouched) { | ||||
|             this.setState({ | ||||
|                 isTouched: true | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         const form = event.currentTarget; | ||||
|  | ||||
|         if (form.checkValidity()) { | ||||
|             this.props.onSubmit(); | ||||
|         } else { | ||||
|             const firstError = form.querySelectorAll(':invalid')[0]; | ||||
|             firstError.focus(); | ||||
|  | ||||
|             let errorMessage = firstError.validationMessage; | ||||
|             if (firstError.validity.valueMissing) { | ||||
|                 errorMessage = `error.${firstError.name}_required`; | ||||
|             } else if (firstError.validity.typeMismatch) { | ||||
|                 errorMessage = `error.${firstError.name}_invalid`; | ||||
|             } | ||||
|  | ||||
|             this.props.onInvalid(errorMessage); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| import React from 'react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
|  | ||||
| import classNames from 'classnames'; | ||||
|  | ||||
| import styles from './panel.scss'; | ||||
| import icons from './icons.scss'; | ||||
| @@ -56,21 +58,41 @@ export function PanelFooter(props) { | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export function PanelBodyHeader(props) { | ||||
|     var { type = 'default' } = props; | ||||
| export class PanelBodyHeader extends Component { | ||||
|     static displayName = 'PanelBodyHeader'; | ||||
|  | ||||
|     var close; | ||||
|     static propTypes = { | ||||
|         type: PropTypes.oneOf(['default', 'error']), | ||||
|         onClose: PropTypes.func | ||||
|     }; | ||||
|  | ||||
|     if (type === 'error') { | ||||
|         close = ( | ||||
|             <span className={styles.close} /> | ||||
|     render() { | ||||
|         const {type = 'default', children} = this.props; | ||||
|  | ||||
|         let close; | ||||
|         if (type === 'error') { | ||||
|             close = ( | ||||
|                 <span className={styles.close} onClick={this.onClose} /> | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         const className = classNames(styles[`${type}BodyHeader`], { | ||||
|             [styles.isClosed]: this.state && this.state.isClosed | ||||
|         }); | ||||
|  | ||||
|         return ( | ||||
|             <div className={className} {...this.props}> | ||||
|                 {close} | ||||
|                 {children} | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <div className={styles[`${type}BodyHeader`]} {...props}> | ||||
|             {close} | ||||
|             {props.children} | ||||
|         </div> | ||||
|     ); | ||||
|     onClose = (event) => { | ||||
|         event.preventDefault(); | ||||
|  | ||||
|         this.setState({isClosed: true}); | ||||
|  | ||||
|         this.props.onClose(); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -58,3 +58,4 @@ | ||||
|  | ||||
| @include button-theme('blue', $blue); | ||||
| @include button-theme('green', $green); | ||||
| @include button-theme('orange', $orange); | ||||
|   | ||||
| @@ -66,8 +66,7 @@ | ||||
|     } | ||||
|  | ||||
|     &:hover { | ||||
|         border-color: #aaa; | ||||
|  | ||||
|         &, | ||||
|         ~ .formFieldIcon { | ||||
|             border-color: #aaa; | ||||
|         } | ||||
| @@ -95,12 +94,14 @@ | ||||
|     text-align: center; | ||||
|     border: 2px solid lighter($black); | ||||
|     color: #444; | ||||
|     cursor: default; | ||||
|  | ||||
|     @include form-transition(); | ||||
| } | ||||
|  | ||||
| @include input-theme('green', $green); | ||||
| @include input-theme('blue', $blue); | ||||
| @include input-theme('red', $red); | ||||
|  | ||||
|  | ||||
| /** | ||||
| @@ -190,3 +191,44 @@ | ||||
|  | ||||
| @include checkbox-theme('green', $green); | ||||
| @include checkbox-theme('blue', $blue); | ||||
| @include checkbox-theme('red', $red); | ||||
|  | ||||
| /** | ||||
|  * Form validation | ||||
|  */ | ||||
|  | ||||
| .formTouched .textField:invalid { | ||||
|     box-shadow: none; | ||||
|  | ||||
|     &, | ||||
|     ~ .formFieldIcon { | ||||
|         border-color: #3e2727; | ||||
|     } | ||||
|  | ||||
|     ~ .formFieldIcon { | ||||
|         color: #3e2727; | ||||
|     } | ||||
|  | ||||
|     &:hover { | ||||
|         &, | ||||
|         ~ .formFieldIcon { | ||||
|             border-color: $red; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &:focus { | ||||
|         border-color: $red; | ||||
|  | ||||
|         ~ .formFieldIcon { | ||||
|             background: $red; | ||||
|             border-color: $red; | ||||
|             color: #fff; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .formTouched .checkboxInput:invalid { | ||||
|     ~ .checkbox { | ||||
|         border-color: $red; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -74,9 +74,20 @@ $bodyTopBottomPadding: 15px; | ||||
|  | ||||
| .bodyHeader { | ||||
|     position: relative; | ||||
|     overflow: hidden; | ||||
|     padding: 10px; | ||||
|     margin: (-$bodyTopBottomPadding) (-$bodyLeftRightPadding); | ||||
|     margin-bottom: 15px; | ||||
|     max-height: 200px; | ||||
|  | ||||
|     transition: 0.4s ease; | ||||
| } | ||||
|  | ||||
| .isClosed { | ||||
|     max-height: 0; | ||||
|     opacity: 0; | ||||
|     padding: 0; | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| .errorBodyHeader { | ||||
|   | ||||
							
								
								
									
										58
									
								
								src/components/user/User.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/components/user/User.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import { PropTypes } from 'react'; | ||||
|  | ||||
| const KEY_USER = 'user'; | ||||
|  | ||||
| export default class User { | ||||
|     /** | ||||
|      * @param  {Object|string|undefined} data plain object or jwt token or empty to load from storage | ||||
|      * | ||||
|      * @return {User} | ||||
|      */ | ||||
|     constructor(data) { | ||||
|         if (!data) { | ||||
|             return this.load(); | ||||
|         } | ||||
|  | ||||
|         // TODO: strict value types validation | ||||
|  | ||||
|         const defaults = { | ||||
|             id: null, | ||||
|             token: '', | ||||
|             username: '', | ||||
|             email: '', | ||||
|             avatar: '', | ||||
|             isGuest: true, | ||||
|             isActive: false | ||||
|         }; | ||||
|  | ||||
|         const user = Object.keys(defaults).reduce((user, key) => { | ||||
|             if (data.hasOwnProperty(key)) { | ||||
|                 user[key] = data[key]; | ||||
|             } | ||||
|  | ||||
|             return user; | ||||
|         }, defaults); | ||||
|  | ||||
|         localStorage.setItem(KEY_USER, JSON.stringify(user)); | ||||
|  | ||||
|         return user; | ||||
|     } | ||||
|  | ||||
|     load() { | ||||
|         try { | ||||
|             return new User(JSON.parse(localStorage.getItem(KEY_USER))); | ||||
|         } catch (error) { | ||||
|             return new User({isGuest: true}); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const userShape = PropTypes.shape({ | ||||
|     id: PropTypes.number, | ||||
|     token: PropTypes.string, | ||||
|     username: PropTypes.string, | ||||
|     email: PropTypes.string, | ||||
|     avatar: PropTypes.string, | ||||
|     isGuest: PropTypes.bool.isRequired, | ||||
|     isActive: PropTypes.bool.isRequired | ||||
| }); | ||||
							
								
								
									
										23
									
								
								src/components/user/actions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/components/user/actions.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| export const UPDATE = 'USER_UPDATE'; | ||||
| /** | ||||
|  * @param  {string|Object} payload jwt token or user object | ||||
|  * @return {Object} action definition | ||||
|  */ | ||||
| export function updateUser(payload) { | ||||
|     return { | ||||
|         type: UPDATE, | ||||
|         payload | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export const SET = 'USER_SET'; | ||||
| export function setUser(payload) { | ||||
|     return { | ||||
|         type: SET, | ||||
|         payload | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export function logout() { | ||||
|     return setUser({isGuest: true}); | ||||
| } | ||||
							
								
								
									
										25
									
								
								src/components/user/reducer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/components/user/reducer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { UPDATE, SET } from './actions'; | ||||
|  | ||||
| import User from './User'; | ||||
|  | ||||
| export default function user( | ||||
|     state = new User(), | ||||
|     {type, payload = null} | ||||
| ) { | ||||
|     switch (type) { | ||||
|         case UPDATE: | ||||
|             if (!payload) { | ||||
|                 throw new Error('payload is required for user reducer'); | ||||
|             } | ||||
|  | ||||
|             return new User({ | ||||
|                 ...state, | ||||
|                 ...payload | ||||
|             }); | ||||
|         case SET: | ||||
|             return new User(payload || {}); | ||||
|  | ||||
|         default: | ||||
|             return state; | ||||
|     } | ||||
| } | ||||
| @@ -8,16 +8,31 @@ import buttons from 'components/ui/buttons.scss'; | ||||
| import messages from './Userbar.messages.js'; | ||||
| import styles from './userbar.scss'; | ||||
|  | ||||
| import { userShape } from 'components/user/User'; | ||||
|  | ||||
| export default class Userbar extends Component { | ||||
|     static displayName = 'Userbar'; | ||||
|     static propTypes = { | ||||
|         user: userShape | ||||
|     }; | ||||
|  | ||||
|     render() { | ||||
|         const { user } = this.props; | ||||
|  | ||||
|         return ( | ||||
|             <div className={styles.userbar}> | ||||
|                 <Link to="/register" className={buttons.blue}> | ||||
|                     <Message {...messages.register} /> | ||||
|                 </Link> | ||||
|                 <Link to="/oauth/permissions" className={buttons.blue}> | ||||
|                     Test oAuth | ||||
|                 </Link> | ||||
|                 {user.isGuest | ||||
|                     ? ( | ||||
|                         <Link to="/register" className={buttons.blue}> | ||||
|                             <Message {...messages.register} /> | ||||
|                         </Link> | ||||
|                     ) | ||||
|                     : ( | ||||
|                         <Link to="/logout" className={buttons.blue}> | ||||
|                             <Message {...messages.logout} /> | ||||
|                         </Link> | ||||
|                     ) | ||||
|                 } | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|   | ||||
| @@ -4,5 +4,10 @@ export default defineMessages({ | ||||
|     register: { | ||||
|         id: 'register', | ||||
|         defaultMessage: 'Join' | ||||
|     }, | ||||
|  | ||||
|     logout: { | ||||
|         id: 'logout', | ||||
|         defaultMessage: 'Logout' | ||||
|     } | ||||
| }); | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import { syncHistory, routeReducer } from 'react-router-redux'; | ||||
| import { IntlProvider } from 'react-intl'; | ||||
|  | ||||
| import reducers from 'reducers'; | ||||
| import routes from 'routes'; | ||||
| import routesFactory from 'routes'; | ||||
|  | ||||
| import 'index.scss'; | ||||
|  | ||||
| @@ -51,7 +51,7 @@ ReactDOM.render( | ||||
|     <IntlProvider locale="en" messages={{}}> | ||||
|         <ReduxProvider store={store}> | ||||
|             <Router history={browserHistory}> | ||||
|                 {routes} | ||||
|                 {routesFactory(store)} | ||||
|             </Router> | ||||
|         </ReduxProvider> | ||||
|     </IntlProvider>, | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
|  | ||||
| import AppInfo from 'components/auth/AppInfo'; | ||||
| import PanelTransition from 'components/auth/PanelTransition'; | ||||
|  | ||||
| import styles from './auth.scss'; | ||||
|  | ||||
| class AuthPage extends Component { | ||||
| export default class AuthPage extends Component { | ||||
|     static displayName = 'AuthPage'; | ||||
|  | ||||
|     state = { | ||||
| @@ -39,7 +38,3 @@ class AuthPage extends Component { | ||||
|         }); | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export default connect((state) => ({ | ||||
|     path: state.routing.location.pathname | ||||
| }))(AuthPage); | ||||
|   | ||||
| @@ -8,7 +8,7 @@ $sidebar-width: 320px; | ||||
|     right: 0; | ||||
|     left: 0; | ||||
|     top: 50px; | ||||
|     z-index: 1; | ||||
|     z-index: 10; | ||||
|  | ||||
|     background: $black; | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| import React from 'react'; | ||||
| import React, { PropTypes } from 'react'; | ||||
|  | ||||
| import { connect } from 'react-redux'; | ||||
| import { Link } from 'react-router'; | ||||
|  | ||||
| import Userbar from 'components/userbar/Userbar'; | ||||
|  | ||||
| import styles from './root.scss'; | ||||
|  | ||||
| export default function RootPage(props) { | ||||
| function RootPage(props) { | ||||
|     return ( | ||||
|         <div className={styles.root}> | ||||
|             <div className={styles.header}> | ||||
| @@ -15,7 +16,7 @@ export default function RootPage(props) { | ||||
|                         Ely.by | ||||
|                     </Link> | ||||
|                     <div className={styles.userbar}> | ||||
|                         <Userbar /> | ||||
|                         <Userbar {...props} /> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
| @@ -25,3 +26,12 @@ export default function RootPage(props) { | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| RootPage.displayName = 'RootPage'; | ||||
| RootPage.propTypes = { | ||||
|     children: PropTypes.element | ||||
| }; | ||||
|  | ||||
| export default connect((state) => ({ | ||||
|     user: state.user | ||||
| }))(RootPage); | ||||
|   | ||||
| @@ -1,2 +1,7 @@ | ||||
| import auth from 'components/auth/reducer'; | ||||
| import user from 'components/user/reducer'; | ||||
|  | ||||
| export default { | ||||
|     auth, | ||||
|     user | ||||
| }; | ||||
|   | ||||
| @@ -10,29 +10,46 @@ import Login from 'components/auth/Login'; | ||||
| import Permissions from 'components/auth/Permissions'; | ||||
| import Activation from 'components/auth/Activation'; | ||||
| import Password from 'components/auth/Password'; | ||||
| import Logout from 'components/auth/Logout'; | ||||
|  | ||||
| function requireAuth(nextState, replace) { | ||||
|     // if (!auth.loggedIn()) { | ||||
|         replace({ | ||||
|             pathname: '/login', | ||||
|             state: { | ||||
|                 nextPathname: nextState.location.pathname | ||||
| export default function routesFactory(store) { | ||||
|     function checkAuth(nextState, replace) { | ||||
|         const state = store.getState(); | ||||
|  | ||||
|         let forcePath; | ||||
|         if (!state.user.isGuest) { | ||||
|             if (!state.user.isActive) { | ||||
|                 forcePath = '/activation'; | ||||
|             } else { | ||||
|                 forcePath = '/oauth/permissions'; | ||||
|             } | ||||
|         }); | ||||
|     // } | ||||
|         } else { | ||||
|             if (state.user.email || state.user.username) { | ||||
|                 forcePath = '/password'; | ||||
|             } else { | ||||
|                 forcePath = '/login'; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (forcePath && state.routing.location.pathname !== forcePath) { | ||||
|             replace({pathname: forcePath}); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <Route path="/" component={RootPage}> | ||||
|             <IndexRoute component={IndexPage} onEnter={checkAuth} /> | ||||
|  | ||||
|             <Route path="auth" component={AuthPage}> | ||||
|                 <Route path="/login" components={new Login()} onEnter={checkAuth} /> | ||||
|                 <Route path="/password" components={new Password()} onEnter={checkAuth} /> | ||||
|                 <Route path="/register" components={new Register()} /> | ||||
|                 <Route path="/activation" components={new Activation()} /> | ||||
|                 <Route path="/oauth/permissions" components={new Permissions()} onEnter={checkAuth} /> | ||||
|                 <Route path="/oauth/:id" component={Permissions} /> | ||||
|             </Route> | ||||
|  | ||||
|             <Route path="logout" component={Logout} /> | ||||
|         </Route> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export default ( | ||||
|   <Route path="/" component={RootPage}> | ||||
|     <IndexRoute component={IndexPage} onEnter={requireAuth} /> | ||||
|  | ||||
|     <Route path="auth" component={AuthPage}> | ||||
|         <Route path="/login" components={new Login()} /> | ||||
|         <Route path="/password" components={new Password()} /> | ||||
|         <Route path="/register" components={new Register()} /> | ||||
|         <Route path="/activation" components={new Activation()} /> | ||||
|         <Route path="/oauth/permissions" components={new Permissions()} /> | ||||
|         <Route path="/oauth/:id" component={Permissions} /> | ||||
|     </Route> | ||||
|   </Route> | ||||
| ); | ||||
|   | ||||
							
								
								
									
										27
									
								
								src/services/request.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/services/request.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| function serialize(data) { | ||||
|     return Object.keys(data) | ||||
|         .map( | ||||
|             (keyName) => | ||||
|                 [keyName, data[keyName]] | ||||
|                     .map(encodeURIComponent) | ||||
|                     .join('=') | ||||
|         ) | ||||
|         .join('&') | ||||
|         ; | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     post(url, data) { | ||||
|         return fetch(url, { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 Accept: 'application/json', | ||||
|                 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' | ||||
|             }, | ||||
|             body: serialize(data) | ||||
|         }) | ||||
|         .then((resp) => resp.json()) | ||||
|         .then((resp) => Promise[resp.success ? 'resolve' : 'reject'](resp)) | ||||
|         ; | ||||
|     } | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user