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

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 { FormattedMessage as Message } from 'react-intl';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import buttons from 'components/ui/buttons.scss'; import buttons from 'components/ui/buttons.scss';
import { Panel, PanelBody, PanelFooter } from 'components/ui/Panel';
import { Input } from 'components/ui/Form'; import { Input } from 'components/ui/Form';
import BaseAuthBody from './BaseAuthBody';
import styles from './activation.scss'; import styles from './activation.scss';
import {helpLinks as helpLinksStyles} from './helpLinks.scss';
import messages from './Activation.messages'; import messages from './Activation.messages';
export default function Activation() { class Body extends BaseAuthBody {
var Title = () => ( // TODO: separate component for PageTitle static propTypes = {
<Message {...messages.accountActivationTitle}> ...BaseAuthBody.propTypes,
{(msg) => <span>{msg}<Helmet title={msg} /></span>} activate: PropTypes.func.isRequired,
</Message> auth: PropTypes.shape({
); error: PropTypes.string,
Title.goBack = '/register'; login: PropTypes.shape({
login: PropTypes.stirng
})
})
};
return { render() {
Title, return (
Body: () => (
<div> <div>
{this.renderErrors()}
<div className={styles.description}> <div className={styles.description}>
<div className={styles.descriptionImage} /> <div className={styles.descriptionImage} />
<div className={styles.descriptionText}> <div className={styles.descriptionText}>
<Message {...messages.activationMailWasSent} values={{ <Message {...messages.activationMailWasSent} values={{
email: (<b>erickskrauch@yandex.ru</b>) email: (<b>{this.props.user.email}</b>)
}} /> }} />
</div> </div>
</div> </div>
<div className={styles.formRow}> <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>
</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} /> <Message {...messages.confirmEmail} />
</button> </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 React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { routeActions } from 'react-router-redux';
import { FormattedMessage as Message } from 'react-intl'; import { FormattedMessage as Message } from 'react-intl';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import buttons from 'components/ui/buttons.scss'; import buttons from 'components/ui/buttons.scss';
import { Panel, PanelBody, PanelFooter } from 'components/ui/Panel';
import { Input } from 'components/ui/Form'; import { Input } from 'components/ui/Form';
import BaseAuthBody from './BaseAuthBody';
import messages from './Login.messages'; import messages from './Login.messages';
import {helpLinks as helpLinksStyles} from './helpLinks.scss';
import passwordMessages from './Password.messages'; import passwordMessages from './Password.messages';
export default function Login() { class Body extends BaseAuthBody {
var context = { static propTypes = {
onSubmit(event) { ...BaseAuthBody.propTypes,
event.preventDefault(); login: PropTypes.func.isRequired,
auth: PropTypes.shape({
this.props.push('/password'); 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 { return {
Title: () => ( // TODO: separate component for PageTitle Title: () => ( // TODO: separate component for PageTitle
<Message {...messages.loginTitle}> <Message {...messages.loginTitle}>
{(msg) => <span>{msg}<Helmet title={msg} /></span>} {(msg) => <span>{msg}<Helmet title={msg} /></span>}
</Message> </Message>
), ),
Body: () => <Input icon="envelope" type="email" placeholder={messages.emailOrUsername} />, Body,
Footer: (props) => ( Footer: () => (
<button className={buttons.green} onClick={(event) => { <button className={buttons.green} type="submit">
event.preventDefault();
props.history.push('/password');
}}>
<Message {...messages.next} /> <Message {...messages.next} />
</button> </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 { TransitionMotion, spring } from 'react-motion';
import ReactHeight from 'react-height'; import ReactHeight from 'react-height';
import { Panel, PanelBody, PanelFooter, PanelHeader } from 'components/ui/Panel'; 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 {helpLinks as helpLinksStyles} from 'components/auth/helpLinks.scss';
import panelStyles from 'components/ui/panel.scss'; import panelStyles from 'components/ui/panel.scss';
import icons from 'components/ui/icons.scss'; import icons from 'components/ui/icons.scss';
import * as actions from './actions';
const opacitySpringConfig = [300, 20]; const opacitySpringConfig = [300, 20];
const transformSpringConfig = [500, 50]; const transformSpringConfig = [500, 50];
const changeContextSpringConfig = [500, 20]; 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 = { state = {
height: {}, height: {},
contextHeight: 0 contextHeight: 0
}; };
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
var previousRoute = this.props.location; var nextPath = nextProps.path;
var previousPath = this.props.path;
var next = nextProps.path; if (nextPath !== previousPath) {
var prev = previousRoute && previousRoute.pathname; var direction = this.getDirection(nextPath, previousPath);
var forceHeight = direction === 'Y' && nextPath !== previousPath ? 1 : 0;
var direction = this.getDirection(next, next, prev); this.props.clearErrors();
var forceHeight = direction === 'Y' && next !== prev ? 1 : 0; this.setState({
direction,
forceHeight,
previousPath
});
this.setState({ if (forceHeight) {
direction, setTimeout(() => {
forceHeight, this.setState({forceHeight: 0});
previousRoute }, 100);
}); }
if (forceHeight) {
setTimeout(() => {
this.setState({forceHeight: 0});
}, 100);
} }
} }
render() { render() {
var {previousRoute, height, contextHeight, forceHeight} = this.state; const {height, canAnimateHeight, contextHeight, forceHeight} = this.state;
const {path, Title, Body, Footer, Links} = this.props; const {path, Title, Body, Footer, Links} = this.props;
@ -54,7 +79,7 @@ export default class PanelTransition extends Component {
Body, Body,
Footer, Footer,
Links, Links,
hasBackButton: previousRoute && previousRoute.pathname === Title.type.goBack, hasBackButton: Title.type.goBack,
transformSpring: spring(0, transformSpringConfig), transformSpring: spring(0, transformSpringConfig),
opacitySpring: spring(1, opacitySpringConfig) opacitySpring: spring(1, opacitySpringConfig)
}, },
@ -67,24 +92,28 @@ export default class PanelTransition extends Component {
willLeave={this.willLeave} willLeave={this.willLeave}
> >
{(items) => { {(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 ( return (
<div> <Form id={path} onSubmit={this.onFormSubmit} onInvalid={this.onFormInvalid}>
<Panel> <Panel>
<PanelHeader> <PanelHeader>
{keys.map((key) => this.getHeader(key, items[key]))} {keys.map((key) => this.getHeader(key, items[key]))}
</PanelHeader> </PanelHeader>
<div style={{ <div style={contentHeight}>
overflow: 'hidden', <ReactHeight onHeightReady={this.onUpdateContextHeight}>
height: forceHeight ? items.common.switchContextHeightSpring : 'auto'
}}>
<ReactHeight onHeightReady={this.updateContextHeight}>
<PanelBody> <PanelBody>
<div style={{ <div style={bodyHeight}>
position: 'relative',
height: `${previousRoute ? items.common.heightSpring : height[path]}px`
}}>
{keys.map((key) => this.getBody(key, items[key]))} {keys.map((key) => this.getBody(key, items[key]))}
</div> </div>
</PanelBody> </PanelBody>
@ -97,21 +126,24 @@ export default class PanelTransition extends Component {
<div className={helpLinksStyles}> <div className={helpLinksStyles}>
{keys.map((key) => this.getLinks(key, items[key]))} {keys.map((key) => this.getLinks(key, items[key]))}
</div> </div>
</div> </Form>
); );
}} }}
</TransitionMotion> </TransitionMotion>
); );
} }
willEnter = (key, styles) => { onFormSubmit = () => {
return this.getTransitionStyles(key, styles); this.body.onFormSubmit();
}; };
willLeave = (key, styles) => { onFormInvalid = (errorMessage) => {
return this.getTransitionStyles(key, styles, {isLeave: true}); this.props.setError(errorMessage);
}; };
willEnter = (key, styles) => this.getTransitionStyles(key, styles);
willLeave = (key, styles) => this.getTransitionStyles(key, styles, {isLeave: true});
/** /**
* @param {string} key * @param {string} key
* @param {Object} styles * @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({ this.setState({
canAnimateHeight,
height: { height: {
...this.state.height, ...this.state.height,
[this.props.path]: height [this.props.path]: height
@ -149,7 +184,7 @@ export default class PanelTransition extends Component {
}); });
}; };
updateContextHeight = (height) => { onUpdateContextHeight = (height) => {
this.setState({ this.setState({
contextHeight: height contextHeight: height
}); });
@ -158,10 +193,11 @@ export default class PanelTransition extends Component {
onGoBack = (event) => { onGoBack = (event) => {
event.preventDefault(); 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 not = (path) => prev !== path && next !== path;
var map = { var map = {
@ -172,7 +208,7 @@ export default class PanelTransition extends Component {
'/oauth/permissions': 'Y' '/oauth/permissions': 'Y'
}; };
return map[key]; return map[next];
} }
getHeader(key, props) { getHeader(key, props) {
@ -192,7 +228,7 @@ export default class PanelTransition extends Component {
}; };
var backButton = ( 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} /> <span className={icons.arrowLeft} />
</button> </button>
); );
@ -201,7 +237,7 @@ export default class PanelTransition extends Component {
<div key={`header${key}`} style={style}> <div key={`header${key}`} style={style}>
{hasBackButton ? backButton : null} {hasBackButton ? backButton : null}
<div style={scrollStyle}> <div style={scrollStyle}>
{Title} {React.cloneElement(Title, this.props)}
</div> </div>
</div> </div>
); );
@ -228,8 +264,13 @@ export default class PanelTransition extends Component {
}; };
return ( return (
<ReactHeight key={`body${key}`} style={style} onHeightReady={this.updateHeight}> <ReactHeight key={`body${key}`} style={style} onHeightReady={this.onUpdateHeight}>
{Body} {React.cloneElement(Body, {
...this.props,
ref: (body) => {
this.body = body;
}
})}
</ReactHeight> </ReactHeight>
); );
} }
@ -241,7 +282,7 @@ export default class PanelTransition extends Component {
return ( return (
<div key={`footer${key}`} style={style}> <div key={`footer${key}`} style={style}>
{Footer} {React.cloneElement(Footer, this.props)}
</div> </div>
); );
} }
@ -253,7 +294,7 @@ export default class PanelTransition extends Component {
return ( return (
<div key={`links${key}`} style={style}> <div key={`links${key}`} style={style}>
{Links} {React.cloneElement(Links, this.props)}
</div> </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 { FormattedMessage as Message } from 'react-intl';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import buttons from 'components/ui/buttons.scss'; import buttons from 'components/ui/buttons.scss';
import icons from 'components/ui/icons.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 { Input, Checkbox } from 'components/ui/Form';
import BaseAuthBody from './BaseAuthBody';
import styles from './password.scss'; import styles from './password.scss';
import {helpLinks as helpLinksStyles} from './helpLinks.scss';
import messages from './Password.messages'; 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() { export default function Password() {
var Title = () => ( // TODO: separate component for PageTitle var Title = () => ( // TODO: separate component for PageTitle
<Message {...messages.passwordTitle}> <Message {...messages.passwordTitle}>
{(msg) => <span>{msg}<Helmet title={msg} /></span>} {(msg) => <span>{msg}<Helmet title={msg} /></span>}
</Message> </Message>
); );
Title.goBack = '/login'; Title.goBack = true;
return { return {
Title, Title,
Body: () => ( Body,
<div> Footer: () => (
<PanelBodyHeader type="error"> <button className={buttons.green} type="submit">
<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');
}}>
<Message {...messages.signInButton} /> <Message {...messages.signInButton} />
</button> </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 { FormattedMessage as Message } from 'react-intl';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import buttons from 'components/ui/buttons.scss'; import buttons from 'components/ui/buttons.scss';
import icons from 'components/ui/icons.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 styles from './permissions.scss';
import {helpLinks as helpLinksStyles} from './helpLinks.scss';
import messages from './Permissions.messages'; import messages from './Permissions.messages';
export default function Permissions() { class Body extends BaseAuthBody {
return { static propTypes = {
Title: () => ( // TODO: separate component for PageTitle ...BaseAuthBody.propTypes,
<Message {...messages.permissionsTitle}> login: PropTypes.func.isRequired,
{(msg) => <span>{msg}<Helmet title={msg} /></span>} auth: PropTypes.shape({
</Message> error: PropTypes.string,
), login: PropTypes.shape({
Body: () => ( login: PropTypes.stirng
})
})
};
render() {
return (
<div> <div>
{this.renderErrors()}
<PanelBodyHeader> <PanelBodyHeader>
<div className={styles.authInfo}> <div className={styles.authInfo}>
<div className={styles.authInfoAvatar}> <div className={styles.authInfoAvatar}>
@ -30,7 +38,7 @@ export default function Permissions() {
<Message {...messages.youAuthorizedAs} /> <Message {...messages.youAuthorizedAs} />
</div> </div>
<div className={styles.authInfoEmail}> <div className={styles.authInfoEmail}>
erickskrauch@yandex.ru {'erickskrauch@yandex.ru'}
</div> </div>
</div> </div>
</PanelBodyHeader> </PanelBodyHeader>
@ -47,9 +55,24 @@ export default function Permissions() {
</ul> </ul>
</div> </div>
</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: () => ( Footer: () => (
<button className={buttons.green}> <button className={buttons.orange} autoFocus>
<Message {...messages.approve} /> <Message {...messages.approve} />
</button> </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 { FormattedMessage as Message } from 'react-intl';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import buttons from 'components/ui/buttons.scss'; import buttons from 'components/ui/buttons.scss';
import { Panel, PanelBody, PanelFooter } from 'components/ui/Panel';
import { Input, Checkbox } from 'components/ui/Form'; import { Input, Checkbox } from 'components/ui/Form';
import {helpLinks as helpLinksStyles} from './helpLinks.scss'; import BaseAuthBody from './BaseAuthBody';
import messages from './Register.messages'; import messages from './Register.messages';
import activationMessages from './Activation.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() { export default function Register() {
return { return {
Title: () => ( // TODO: separate component for PageTitle Title: () => ( // TODO: separate component for PageTitle
@ -18,30 +95,9 @@ export default function Register() {
{(msg) => <span>{msg}<Helmet title={msg} /></span>} {(msg) => <span>{msg}<Helmet title={msg} /></span>}
</Message> </Message>
), ),
Body: () => ( Body,
<div> Footer: () => (
<Input icon="user" color="blue" type="text" placeholder={messages.yourNickname} /> <button className={buttons.blue} type="submit">
<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');
}}>
<Message {...messages.signUpButton} /> <Message {...messages.signUpButton} />
</button> </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 classNames from 'classnames';
import {injectIntl, intlShape} from 'react-intl'; import { intlShape } from 'react-intl';
import icons from './icons.scss'; import icons from './icons.scss';
import styles from './form.scss'; import styles from './form.scss';
function Input(props) { export class Input extends Component {
var { icon, color = 'green' } = props; static displayName = 'Input';
props = { static propTypes = {
type: 'text', placeholder: PropTypes.shape({
...props id: PropTypes.string
}),
icon: PropTypes.string,
color: PropTypes.oneOf(['green', 'blue', 'red'])
}; };
if (props.placeholder && props.placeholder.id) { static contextTypes = {
props.placeholder = props.intl.formatMessage(props.placeholder); intl: intlShape.isRequired
} };
var baseClass = styles.formRow; render() {
if (icon) { let { icon, color = 'green' } = this.props;
baseClass = styles.formIconRow;
icon = ( const props = {
<div className={classNames(styles.formFieldIcon, icons[icon])} /> 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 ( setEl = (el) => {
<div className={baseClass}> this.el = el;
<input className={styles[`${color}TextField`]} {...props} /> };
{icon}
</div> getValue() {
); return this.el.value;
}
} }
Input.displayName = 'Input'; export class Checkbox extends Component {
Input.propTypes = { static displayName = 'Checkbox';
intl: intlShape.isRequired
};
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) { return (
var { label, color = 'green' } = props; <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 ( setEl = (el) => {
<div className={styles[`${color}CheckboxRow`]}> this.el = el;
<label className={styles.checkboxContainer}> };
<input className={styles.checkboxInput} type="checkbox" />
<div className={styles.checkbox} /> getValue() {
{label} return this.el.checked ? 1 : 0;
</label> }
</div>
);
} }
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 styles from './panel.scss';
import icons from './icons.scss'; import icons from './icons.scss';
@ -56,21 +58,41 @@ export function PanelFooter(props) {
); );
} }
export function PanelBodyHeader(props) { export class PanelBodyHeader extends Component {
var { type = 'default' } = props; static displayName = 'PanelBodyHeader';
var close; static propTypes = {
type: PropTypes.oneOf(['default', 'error']),
onClose: PropTypes.func
};
if (type === 'error') { render() {
close = ( const {type = 'default', children} = this.props;
<span className={styles.close} />
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 ( onClose = (event) => {
<div className={styles[`${type}BodyHeader`]} {...props}> event.preventDefault();
{close}
{props.children} this.setState({isClosed: true});
</div>
); this.props.onClose();
};
} }

View File

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

View File

@ -66,8 +66,7 @@
} }
&:hover { &:hover {
border-color: #aaa; &,
~ .formFieldIcon { ~ .formFieldIcon {
border-color: #aaa; border-color: #aaa;
} }
@ -95,12 +94,14 @@
text-align: center; text-align: center;
border: 2px solid lighter($black); border: 2px solid lighter($black);
color: #444; color: #444;
cursor: default;
@include form-transition(); @include form-transition();
} }
@include input-theme('green', $green); @include input-theme('green', $green);
@include input-theme('blue', $blue); @include input-theme('blue', $blue);
@include input-theme('red', $red);
/** /**
@ -190,3 +191,44 @@
@include checkbox-theme('green', $green); @include checkbox-theme('green', $green);
@include checkbox-theme('blue', $blue); @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 { .bodyHeader {
position: relative; position: relative;
overflow: hidden;
padding: 10px; padding: 10px;
margin: (-$bodyTopBottomPadding) (-$bodyLeftRightPadding); margin: (-$bodyTopBottomPadding) (-$bodyLeftRightPadding);
margin-bottom: 15px; margin-bottom: 15px;
max-height: 200px;
transition: 0.4s ease;
}
.isClosed {
max-height: 0;
opacity: 0;
padding: 0;
margin: 0;
} }
.errorBodyHeader { .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 messages from './Userbar.messages.js';
import styles from './userbar.scss'; import styles from './userbar.scss';
import { userShape } from 'components/user/User';
export default class Userbar extends Component { export default class Userbar extends Component {
static displayName = 'Userbar';
static propTypes = {
user: userShape
};
render() { render() {
const { user } = this.props;
return ( return (
<div className={styles.userbar}> <div className={styles.userbar}>
<Link to="/register" className={buttons.blue}> {user.isGuest
<Message {...messages.register} /> ? (
</Link> <Link to="/register" className={buttons.blue}>
<Link to="/oauth/permissions" className={buttons.blue}> <Message {...messages.register} />
Test oAuth </Link>
</Link> )
: (
<Link to="/logout" className={buttons.blue}>
<Message {...messages.logout} />
</Link>
)
}
</div> </div>
); );
} }

View File

@ -4,5 +4,10 @@ export default defineMessages({
register: { register: {
id: 'register', id: 'register',
defaultMessage: 'Join' 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 { IntlProvider } from 'react-intl';
import reducers from 'reducers'; import reducers from 'reducers';
import routes from 'routes'; import routesFactory from 'routes';
import 'index.scss'; import 'index.scss';
@ -51,7 +51,7 @@ ReactDOM.render(
<IntlProvider locale="en" messages={{}}> <IntlProvider locale="en" messages={{}}>
<ReduxProvider store={store}> <ReduxProvider store={store}>
<Router history={browserHistory}> <Router history={browserHistory}>
{routes} {routesFactory(store)}
</Router> </Router>
</ReduxProvider> </ReduxProvider>
</IntlProvider>, </IntlProvider>,

View File

@ -1,12 +1,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import AppInfo from 'components/auth/AppInfo'; import AppInfo from 'components/auth/AppInfo';
import PanelTransition from 'components/auth/PanelTransition'; import PanelTransition from 'components/auth/PanelTransition';
import styles from './auth.scss'; import styles from './auth.scss';
class AuthPage extends Component { export default class AuthPage extends Component {
static displayName = 'AuthPage'; static displayName = 'AuthPage';
state = { 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; right: 0;
left: 0; left: 0;
top: 50px; top: 50px;
z-index: 1; z-index: 10;
background: $black; 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 { Link } from 'react-router';
import Userbar from 'components/userbar/Userbar'; import Userbar from 'components/userbar/Userbar';
import styles from './root.scss'; import styles from './root.scss';
export default function RootPage(props) { function RootPage(props) {
return ( return (
<div className={styles.root}> <div className={styles.root}>
<div className={styles.header}> <div className={styles.header}>
@ -15,7 +16,7 @@ export default function RootPage(props) {
Ely.by Ely.by
</Link> </Link>
<div className={styles.userbar}> <div className={styles.userbar}>
<Userbar /> <Userbar {...props} />
</div> </div>
</div> </div>
</div> </div>
@ -25,3 +26,12 @@ export default function RootPage(props) {
</div> </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 { export default {
auth,
user
}; };

View File

@ -10,29 +10,46 @@ import Login from 'components/auth/Login';
import Permissions from 'components/auth/Permissions'; import Permissions from 'components/auth/Permissions';
import Activation from 'components/auth/Activation'; import Activation from 'components/auth/Activation';
import Password from 'components/auth/Password'; import Password from 'components/auth/Password';
import Logout from 'components/auth/Logout';
function requireAuth(nextState, replace) { export default function routesFactory(store) {
// if (!auth.loggedIn()) { function checkAuth(nextState, replace) {
replace({ const state = store.getState();
pathname: '/login',
state: { let forcePath;
nextPathname: nextState.location.pathname 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))
;
}
};