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