В первом приближении заинтегрировался с беком

This commit is contained in:
SleepWalker 2016-02-13 17:28:47 +02:00
parent 19eec8f7a4
commit a94ddaf131
29 changed files with 1171 additions and 492 deletions

View File

@ -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>
);
}
}

View 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} />
};
}

View 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'
}
});

View 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;
}, {});
}
}

View File

@ -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);

View 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);

View File

@ -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);

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View 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'));
};
}

View 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;
}
}

View File

@ -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);
}
};
}

View File

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

View File

@ -58,3 +58,4 @@
@include button-theme('blue', $blue);
@include button-theme('green', $green);
@include button-theme('orange', $orange);

View File

@ -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;
}
}

View File

@ -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 {

View 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
});

View 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});
}

View 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;
}
}

View File

@ -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>
);
}

View File

@ -4,5 +4,10 @@ export default defineMessages({
register: {
id: 'register',
defaultMessage: 'Join'
},
logout: {
id: 'logout',
defaultMessage: 'Logout'
}
});

View File

@ -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>,

View File

@ -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);

View File

@ -8,7 +8,7 @@ $sidebar-width: 320px;
right: 0;
left: 0;
top: 50px;
z-index: 1;
z-index: 10;
background: $black;
}

View File

@ -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);

View File

@ -1,2 +1,7 @@
import auth from 'components/auth/reducer';
import user from 'components/user/reducer';
export default {
auth,
user
};

View File

@ -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
View 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))
;
}
};