mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
#84: language switching on frontend
This commit is contained in:
@ -3,6 +3,7 @@ import React, { Component, PropTypes } from 'react';
|
|||||||
import { FormattedMessage as Message } from 'react-intl';
|
import { FormattedMessage as Message } from 'react-intl';
|
||||||
|
|
||||||
import { Button } from 'components/ui/form';
|
import { Button } from 'components/ui/form';
|
||||||
|
import { LangMenu } from 'components/langMenu';
|
||||||
|
|
||||||
import styles from './appInfo.scss';
|
import styles from './appInfo.scss';
|
||||||
import messages from './AppInfo.intl.json';
|
import messages from './AppInfo.intl.json';
|
||||||
@ -36,6 +37,10 @@ export default class AppInfo extends Component {
|
|||||||
<div className={styles.goToAuth}>
|
<div className={styles.goToAuth}>
|
||||||
<Button onClick={onGoToAuth} label={messages.goToAuth} />
|
<Button onClick={onGoToAuth} label={messages.goToAuth} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.langMenu}>
|
||||||
|
<LangMenu />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -50,3 +50,10 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.langMenu {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
23
src/components/i18n/IntlProvider.jsx
Normal file
23
src/components/i18n/IntlProvider.jsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
|
import { IntlProvider as OrigIntlProvider } from 'react-intl';
|
||||||
|
|
||||||
|
class IntlProvider extends Component {
|
||||||
|
static displayName = 'IntlProvider';
|
||||||
|
static propTypes = {
|
||||||
|
locale: PropTypes.string.isRequired,
|
||||||
|
messages: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||||
|
children: PropTypes.element
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<OrigIntlProvider {...this.props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
export default connect(({i18n}) => i18n)(IntlProvider);
|
18
src/components/i18n/actions.js
Normal file
18
src/components/i18n/actions.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import i18n from 'services/i18n';
|
||||||
|
|
||||||
|
export const SET_LOCALE = 'SET_LOCALE';
|
||||||
|
export function setLocale(locale) {
|
||||||
|
return (dispatch) => i18n.require(
|
||||||
|
i18n.detectLanguage(locale)
|
||||||
|
).then(({locale, messages}) => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_LOCALE,
|
||||||
|
payload: {
|
||||||
|
locale,
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return locale;
|
||||||
|
});
|
||||||
|
}
|
5
src/components/i18n/index.js
Normal file
5
src/components/i18n/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import IntlProvider from './IntlProvider';
|
||||||
|
|
||||||
|
export {
|
||||||
|
IntlProvider
|
||||||
|
};
|
9
src/components/i18n/reducer.js
Normal file
9
src/components/i18n/reducer.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { SET_LOCALE } from './actions';
|
||||||
|
|
||||||
|
export default function(state = {}, {type, payload}) {
|
||||||
|
if (type === SET_LOCALE) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
140
src/components/langMenu/LangMenu.jsx
Normal file
140
src/components/langMenu/LangMenu.jsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { FormattedMessage as Message } from 'react-intl';
|
||||||
|
|
||||||
|
import icons from 'components/ui/icons.scss';
|
||||||
|
|
||||||
|
import styles from './langMenu.scss';
|
||||||
|
import messages from './langMenu.intl.json';
|
||||||
|
|
||||||
|
const LANGS = {
|
||||||
|
en: 'English',
|
||||||
|
ru: 'Русский'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class LangMenu extends Component {
|
||||||
|
static displayName = 'LangMenu';
|
||||||
|
static propTypes = {
|
||||||
|
showCurrentLang: PropTypes.bool,
|
||||||
|
toggleRef: PropTypes.func,
|
||||||
|
userLang: PropTypes.string,
|
||||||
|
changeLang: PropTypes.func
|
||||||
|
};
|
||||||
|
static defaultProps = {
|
||||||
|
toggleRef: () => {},
|
||||||
|
showCurrentLang: false
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isActive: false
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('click', this.onBodyClick);
|
||||||
|
this.props.toggleRef(this.toggle.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('click', this.onBodyClick);
|
||||||
|
this.props.toggleRef(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {userLang, showCurrentLang} = this.props;
|
||||||
|
const {isActive} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.container, {
|
||||||
|
[styles.withCurrentLang]: showCurrentLang
|
||||||
|
})}>
|
||||||
|
<div className={styles.menuContainer}>
|
||||||
|
<ul className={classNames(styles.menu, {
|
||||||
|
[styles.menuActive]: isActive
|
||||||
|
})}>
|
||||||
|
{Object.keys(LANGS).map((lang) => (
|
||||||
|
<li className={classNames(styles.menuItem, {
|
||||||
|
[styles.activeMenuItem]: lang === userLang
|
||||||
|
})} onClick={this.onChangeLang(lang)} key={lang}>
|
||||||
|
{this.renderLangLabel(lang)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.triggerContainer} onClick={this.onToggle}>
|
||||||
|
<a className={styles.trigger} href="#">
|
||||||
|
{showCurrentLang
|
||||||
|
? this.renderLangLabel(userLang) : (
|
||||||
|
<span>
|
||||||
|
<span className={icons.globe} />
|
||||||
|
{' '}
|
||||||
|
<Message {...messages.siteLanguage} />
|
||||||
|
{' '}
|
||||||
|
<span className={isActive ? styles.triggerArrowBottom : styles.triggerArrowTop} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLangLabel(lang) {
|
||||||
|
const langLabel = LANGS[lang];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span className={styles[`lang${lang[0].toUpperCase() + lang.slice(1)}`]} />
|
||||||
|
{langLabel}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeLang(lang) {
|
||||||
|
return (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.props.changeLang(lang);
|
||||||
|
this.setState({
|
||||||
|
isActive: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onBodyClick = (event) => {
|
||||||
|
if (this.state.isActive) {
|
||||||
|
const el = ReactDOM.findDOMNode(this);
|
||||||
|
|
||||||
|
if (!el.contains(event.target) && el !== event.taget) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// add a small delay for the case someone have alredy called toggle
|
||||||
|
setTimeout(() => this.state.isActive && this.toggle(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onToggle = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.toggle();
|
||||||
|
};
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
// add small delay to skip click event on body
|
||||||
|
setTimeout(() => this.setState({
|
||||||
|
isActive: !this.state.isActive
|
||||||
|
}), 0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { changeLang } from 'components/user/actions';
|
||||||
|
|
||||||
|
export default connect((state) => ({
|
||||||
|
userLang: state.user.lang
|
||||||
|
}), {
|
||||||
|
changeLang
|
||||||
|
})(LangMenu);
|
5
src/components/langMenu/index.js
Normal file
5
src/components/langMenu/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import LangMenu from './LangMenu';
|
||||||
|
|
||||||
|
export {
|
||||||
|
LangMenu
|
||||||
|
};
|
3
src/components/langMenu/langMenu.intl.json
Normal file
3
src/components/langMenu/langMenu.intl.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"siteLanguage": "Site language"
|
||||||
|
}
|
119
src/components/langMenu/langMenu.scss
Normal file
119
src/components/langMenu/langMenu.scss
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuContainer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
width: 150px;
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
background: #fff;
|
||||||
|
border: 5px solid #ddd8ce;
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 0;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.2s ease;
|
||||||
|
transform: scale(0.1);
|
||||||
|
transform-origin: center bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuActive {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.withCurrentLang {
|
||||||
|
.triggerContainer {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuContainer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeMenuItem {
|
||||||
|
background: #efffef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.langIco {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
width: 20px;
|
||||||
|
height: 10px;
|
||||||
|
|
||||||
|
background: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.langEn {
|
||||||
|
composes: langIco;
|
||||||
|
|
||||||
|
background-image: url('icons/flag_en.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.langRu {
|
||||||
|
composes: langIco;
|
||||||
|
|
||||||
|
background-image: url('icons/flag_ru.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 1px dotted #666;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: .25s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-bottom-color: #777;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerContainer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerArrow {
|
||||||
|
font-size: 8px;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerArrowTop {
|
||||||
|
composes: triggerArrow;
|
||||||
|
composes: arrowTop from 'components/ui/icons.scss';
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerArrowBottom {
|
||||||
|
composes: triggerArrow;
|
||||||
|
composes: arrowBottom from 'components/ui/icons.scss';
|
||||||
|
}
|
@ -4,6 +4,8 @@ import { FormattedMessage as Message, FormattedRelative as Relative, FormattedHT
|
|||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
import { userShape } from 'components/user/User';
|
import { userShape } from 'components/user/User';
|
||||||
|
import { LangMenu } from 'components/langMenu';
|
||||||
|
import langMenuMessages from 'components/langMenu/langMenu.intl.json';
|
||||||
|
|
||||||
import ProfileField from './ProfileField';
|
import ProfileField from './ProfileField';
|
||||||
import styles from './profile.scss';
|
import styles from './profile.scss';
|
||||||
@ -74,6 +76,12 @@ export default class Profile extends Component {
|
|||||||
) : ''}
|
) : ''}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ProfileField
|
||||||
|
label={<Message {...langMenuMessages.siteLanguage} />}
|
||||||
|
value={<LangMenu toggleRef={(toggle) => this.langMenuToggle = toggle} showCurrentLang />}
|
||||||
|
onChange={() => this.langMenuToggle()}
|
||||||
|
/>
|
||||||
|
|
||||||
<ProfileField
|
<ProfileField
|
||||||
label={<Message {...messages.twoFactorAuth} />}
|
label={<Message {...messages.twoFactorAuth} />}
|
||||||
value={<Message {...messages.disabled} />}
|
value={<Message {...messages.disabled} />}
|
||||||
@ -82,7 +90,6 @@ export default class Profile extends Component {
|
|||||||
<ProfileField
|
<ProfileField
|
||||||
label={'UUID'}
|
label={'UUID'}
|
||||||
value={user.uuid}
|
value={user.uuid}
|
||||||
readonly
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,13 +9,23 @@ export default class ProfileField extends Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
label: React.PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
label: React.PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
||||||
link: PropTypes.string,
|
link: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
value: React.PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
value: React.PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
||||||
warningMessage: React.PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
warningMessage: React.PropTypes.oneOfType([PropTypes.string, PropTypes.element])
|
||||||
readonly: PropTypes.bool
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {label, value, warningMessage, readonly, link = '#'} = this.props;
|
const {label, value, warningMessage, link, onChange} = this.props;
|
||||||
|
|
||||||
|
let Action = null;
|
||||||
|
|
||||||
|
if (link) {
|
||||||
|
Action = (props) => <Link to={link} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onChange) {
|
||||||
|
Action = (props) => <a onClick={onChange} {...props} href="#" />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.paramItem}>
|
<div className={styles.paramItem}>
|
||||||
@ -23,13 +33,11 @@ export default class ProfileField extends Component {
|
|||||||
<div className={styles.paramName}>{label}:</div>
|
<div className={styles.paramName}>{label}:</div>
|
||||||
<div className={styles.paramValue}>{value}</div>
|
<div className={styles.paramValue}>{value}</div>
|
||||||
|
|
||||||
{readonly ? '' : (
|
{Action ? (
|
||||||
<div className={styles.paramAction}>
|
<Action to={link} className={styles.paramAction}>
|
||||||
<Link to={link}>
|
|
||||||
<span className={styles.paramEditIcon} />
|
<span className={styles.paramEditIcon} />
|
||||||
</Link>
|
</Action>
|
||||||
</div>
|
) : null}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{warningMessage ? (
|
{warningMessage ? (
|
||||||
|
@ -74,6 +74,7 @@
|
|||||||
|
|
||||||
.paramAction {
|
.paramAction {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paramEditIcon {
|
.paramEditIcon {
|
||||||
|
@ -261,7 +261,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[type="submit"] {
|
[type="submit"] {
|
||||||
background: url('images/loader_button.gif') #95a5a6 center center;
|
background: url('./images/loader_button.gif') #95a5a6 center center;
|
||||||
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
@ -8,3 +8,13 @@
|
|||||||
|
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.arrowTop {
|
||||||
|
composes: arrow;
|
||||||
|
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrowBottom {
|
||||||
|
composes: arrow;
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { routeActions } from 'react-router-redux';
|
|||||||
|
|
||||||
import request from 'services/request';
|
import request from 'services/request';
|
||||||
import accounts from 'services/api/accounts';
|
import accounts from 'services/api/accounts';
|
||||||
|
import { setLocale } from 'components/i18n/actions';
|
||||||
|
|
||||||
export const UPDATE = 'USER_UPDATE';
|
export const UPDATE = 'USER_UPDATE';
|
||||||
/**
|
/**
|
||||||
@ -15,6 +16,25 @@ export function updateUser(payload) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CHANGE_LANG = 'USER_CHANGE_LANG';
|
||||||
|
export function changeLang(lang) {
|
||||||
|
return (dispatch, getState) => dispatch(setLocale(lang))
|
||||||
|
.then((lang) => {
|
||||||
|
const {user: {isGuest, lang: oldLang}} = getState();
|
||||||
|
|
||||||
|
if (!isGuest && oldLang !== lang) {
|
||||||
|
accounts.changeLang(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: CHANGE_LANG,
|
||||||
|
payload: {
|
||||||
|
lang
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const SET = 'USER_SET';
|
export const SET = 'USER_SET';
|
||||||
export function setUser(payload) {
|
export function setUser(payload) {
|
||||||
return {
|
return {
|
||||||
@ -26,6 +46,7 @@ export function setUser(payload) {
|
|||||||
export function logout() {
|
export function logout() {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(setUser({isGuest: true}));
|
dispatch(setUser({isGuest: true}));
|
||||||
|
dispatch(changeLang());
|
||||||
dispatch(routeActions.push('/login'));
|
dispatch(routeActions.push('/login'));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -35,6 +56,8 @@ export function fetchUserData() {
|
|||||||
accounts.current()
|
accounts.current()
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
dispatch(updateUser(resp));
|
dispatch(updateUser(resp));
|
||||||
|
|
||||||
|
return dispatch(changeLang(resp.lang));
|
||||||
})
|
})
|
||||||
.catch((resp) => {
|
.catch((resp) => {
|
||||||
/*
|
/*
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { authenticate } from 'components/user/actions';
|
import { authenticate, changeLang } from 'components/user/actions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes User state with the fresh data
|
* Initializes User state with the fresh data
|
||||||
@ -8,15 +8,15 @@ import { authenticate } from 'components/user/actions';
|
|||||||
* @return {Promise} a promise, that resolves in User state
|
* @return {Promise} a promise, that resolves in User state
|
||||||
*/
|
*/
|
||||||
export function factory(store) {
|
export function factory(store) {
|
||||||
const state = store.getState();
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (state.user.token) {
|
const {user} = store.getState();
|
||||||
|
|
||||||
|
if (user.token) {
|
||||||
// authorizing user if it is possible
|
// authorizing user if it is possible
|
||||||
store.dispatch(authenticate(state.user.token))
|
return store.dispatch(authenticate(user.token)).then(resolve, reject);
|
||||||
.then(() => resolve(store.getState().user), reject);
|
|
||||||
} else {
|
|
||||||
resolve(state.user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// auto-detect guests language
|
||||||
|
store.dispatch(changeLang()).then(resolve, reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { UPDATE, SET } from './actions';
|
import { UPDATE, SET, CHANGE_LANG } from './actions';
|
||||||
|
|
||||||
import User from './User';
|
import User from './User';
|
||||||
|
|
||||||
@ -8,6 +8,16 @@ export default function user(
|
|||||||
{type, payload = null}
|
{type, payload = null}
|
||||||
) {
|
) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case CHANGE_LANG:
|
||||||
|
if (!payload || !payload.lang) {
|
||||||
|
throw new Error('payload.lang is required for user reducer');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new User({
|
||||||
|
...state,
|
||||||
|
lang: payload.lang
|
||||||
|
});
|
||||||
|
|
||||||
case UPDATE:
|
case UPDATE:
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw new Error('payload is required for user reducer');
|
throw new Error('payload is required for user reducer');
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
"components.auth.register.termsOfService": "terms of service",
|
"components.auth.register.termsOfService": "terms of service",
|
||||||
"components.auth.register.yourEmail": "Your E-mail",
|
"components.auth.register.yourEmail": "Your E-mail",
|
||||||
"components.auth.register.yourNickname": "Your nickname",
|
"components.auth.register.yourNickname": "Your nickname",
|
||||||
|
"components.langMenu.siteLanguage": "Site language",
|
||||||
"components.profile.accountDescription": "Ely.by account allows you to get access to many Minecraft resources. Please, take care of your account safety. Use secure password and change it regularly.",
|
"components.profile.accountDescription": "Ely.by account allows you to get access to many Minecraft resources. Please, take care of your account safety. Use secure password and change it regularly.",
|
||||||
"components.profile.accountPreferencesTitle": "Ely.by account preferences",
|
"components.profile.accountPreferencesTitle": "Ely.by account preferences",
|
||||||
"components.profile.changePassword.achievementLossWarning": "Are you cherish your game achievements, right?",
|
"components.profile.changePassword.achievementLossWarning": "Are you cherish your game achievements, right?",
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
"components.auth.register.termsOfService": "правилами сервиса",
|
"components.auth.register.termsOfService": "правилами сервиса",
|
||||||
"components.auth.register.yourEmail": "Ваш E-mail",
|
"components.auth.register.yourEmail": "Ваш E-mail",
|
||||||
"components.auth.register.yourNickname": "Желаемый ник",
|
"components.auth.register.yourNickname": "Желаемый ник",
|
||||||
|
"components.langMenu.siteLanguage": "Язык сайта",
|
||||||
"components.profile.accountDescription": "Благодаря аккаунту Ely.by вы можете получить доступ ко многим ресурсам, связанным с Minecraft. Берегите свой аккаунт, используйте надёжный пароль и регулярно его меняйте.",
|
"components.profile.accountDescription": "Благодаря аккаунту Ely.by вы можете получить доступ ко многим ресурсам, связанным с Minecraft. Берегите свой аккаунт, используйте надёжный пароль и регулярно его меняйте.",
|
||||||
"components.profile.accountPreferencesTitle": "Настройки аккаунта Ely.by",
|
"components.profile.accountPreferencesTitle": "Настройки аккаунта Ely.by",
|
||||||
"components.profile.changePassword.achievementLossWarning": "Вы ведь дорожите своими игровыми достижениями?",
|
"components.profile.changePassword.achievementLossWarning": "Вы ведь дорожите своими игровыми достижениями?",
|
||||||
|
Before Width: | Height: | Size: 522 B After Width: | Height: | Size: 522 B |
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 265 B |
41
src/icons/webfont/globe.svg
Normal file
41
src/icons/webfont/globe.svg
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
|
||||||
|
<path d="M94.9,47.9c0-0.2,0-0.4,0-0.7c0-0.5-0.1-0.9-0.1-1.4c0-0.2,0-0.5-0.1-0.7c-0.1-0.6-0.1-1.1-0.2-1.7
|
||||||
|
c-0.1-0.4-0.1-0.9-0.2-1.3c0-0.1,0-0.3-0.1-0.4c-2.6-14-11.6-25.8-23.9-32l0,0h-0.1c-3-1.5-6.3-2.7-9.7-3.5h-0.1
|
||||||
|
c-0.7-0.2-1.5-0.3-2.2-0.5c-0.2,0-0.3-0.1-0.5-0.1c-0.6-0.1-1.3-0.2-1.9-0.3c-0.2,0-0.4-0.1-0.7-0.1c-0.6-0.1-1.2-0.1-1.9-0.2
|
||||||
|
c-0.2,0-0.4,0-0.7,0c-0.8,0-1.7-0.1-2.5-0.1C25.1,4.9,5,25,5,49.9s20.1,45,45,45s45-20.1,45-45C95,49.2,95,48.5,94.9,47.9z
|
||||||
|
M17.4,72.2l-2.3-3.6C15,68.5,15,68.3,15,68.2v-1.3c0-0.1,0-0.3-0.1-0.4l-1.2-1.9c-0.4-0.6-1.2-0.4-1.4,0.2l-0.5,1.9h-0.3
|
||||||
|
c-0.9-2.1-1.6-4.2-2.2-6.4l2.9,1.7c0.2,0.1,0.5,0.1,0.7,0l1-0.5c0.3-0.2,0.7-0.1,0.9,0.1l1.7,1.8c0.2,0.2,0.6,0.3,0.9,0.1l0.7-0.4
|
||||||
|
l-2-3.7c-0.1-0.1-0.2-0.2-0.3-0.3l-4.6-2.9c-0.4-0.2-0.9-0.1-1.1,0.3L10,56.7c-0.1,0.3-0.4,0.4-0.7,0.4H8.8c-0.1,0-0.1,0-0.2,0
|
||||||
|
C8.2,54.7,8,52.3,8,49.9c0-12.7,5.6-24,14.5-31.7h2.1c0.1,0,0.2,0,0.3,0.1l1.3,0.6c0.2,0.1,0.4,0.4,0.4,0.6L26.2,26
|
||||||
|
c0,0.1,0,0.2-0.1,0.3l-2.8,4.8c-0.1,0.2-0.1,0.4,0,0.6l1.6,3.1c0,0.1,0.1,0.2,0.1,0.3v2.1c0,0.1,0,0.2,0.1,0.3l2.2,3.9l0.9-0.6
|
||||||
|
l-1.7-3.2l0.5-0.3l2.4,3.7c0.1,0.1,0.1,0.2,0.1,0.4v2c0,0.2,0.1,0.5,0.3,0.6l3.1,1.8c0.1,0.1,0.2,0.1,0.3,0.1h2.1
|
||||||
|
c0.2,0,0.4,0.1,0.5,0.3l0.7,1c0.1,0.2,0.3,0.3,0.5,0.3h1.3c0.1,0,0.1,0,0.2,0l0.6,0.2c0.2,0.1,0.4,0.2,0.4,0.4l0.5,1.5
|
||||||
|
c0.1,0.2,0.2,0.3,0.3,0.4l3.6,1.8c0.2,0.1,0.4,0.3,0.4,0.6v1c0,0.1,0,0.3-0.1,0.4l-1.4,2c-0.1,0.1-0.1,0.3-0.1,0.4v3.5
|
||||||
|
c0,0.1,0,0.2,0.1,0.3l3,5.5c0,0.1,0.1,0.1,0.2,0.2l3.5,3c0.2,0.1,0.2,0.3,0.2,0.5L49.6,72c0,0.1,0,0.3,0.1,0.4l0.6,0.9
|
||||||
|
c0.1,0.1,0.1,0.2,0.1,0.4l0.2,7.9c0,0.1,0.1,0.3,0.1,0.5l1.2,3.7c0,0.1,0,0.1,0.1,0.2l2.1,3.4c0.1,0.2,0.3,0.3,0.6,0.3h0.8
|
||||||
|
c0.2,0,0.3,0.1,0.5,0.2l0.5,0.5c0.1,0.1,0.3,0.2,0.5,0.2h2.5l-2.4-1.8c-0.2-0.1-0.3-0.3-0.3-0.6v-2.8c0-0.1,6.3-11.6,7.5-13.8
|
||||||
|
c0.1-0.2,0.3-0.3,0.6-0.3H66c0.3,0,0.5-0.2,0.6-0.4l1.7-3.8c0-0.1,0.1-0.2,0.1-0.3v-2.7c0-0.1,0-0.2,0.1-0.3l1.2-2
|
||||||
|
c0.1-0.1,0.1-0.3,0.1-0.5l-0.3-1.2c-0.2-0.9-0.9-1.7-1.8-1.9l-5.4-2.3c-0.1,0-0.2-0.1-0.3-0.2l-1.3-1.6L59.4,52
|
||||||
|
c-0.1-0.2-0.3-0.3-0.5-0.3H57c-0.2,0-0.4-0.1-0.5-0.2l-1.8-2.2c-0.1-0.2-0.3-0.2-0.5-0.2L46.9,49c-0.2,0-0.4,0.1-0.6,0.2l-1,0.8
|
||||||
|
c-0.2,0.2-0.5,0.2-0.7,0.1L42.2,49c-0.2-0.1-0.4-0.3-0.4-0.6v-2c0-0.4-0.3-0.6-0.7-0.6h-1.9l-0.3-0.6c-0.1-0.2-0.2-0.3-0.4-0.3
|
||||||
|
l-2.1-0.5c-0.1,0-0.2-0.1-0.2-0.1l-1.3-0.9c-0.2-0.2-0.3-0.4-0.3-0.7l1.2-4.5c0.1-0.2,0.2-0.4,0.5-0.5l4.9-0.9c0.1,0,0.2,0,0.3,0
|
||||||
|
l1.5,0.7c0.2,0.1,0.3,0.2,0.4,0.4l0.4,1.6c0.1,0.2,0.3,0.4,0.5,0.3l0.5-0.1c0.2-0.1,0.3-0.3,0.3-0.5l-0.2-2.4c0-0.3,0.1-0.5,0.3-0.6
|
||||||
|
l3.4-2.1c0.2-0.1,0.3-0.3,0.3-0.6v-1c0-0.2,0.1-0.4,0.3-0.5l5.5-3.9C54.8,28,54.9,28,55,28l4-0.6v-1h-0.7c-0.4,0-0.7-0.3-0.7-0.7
|
||||||
|
l0.1-0.9c0-0.3,0.3-0.6,0.6-0.6h0.9c0.4,0,0.7,0.3,0.7,0.6v0.9c0,0.4,0.3,0.6,0.7,0.6H63c0.4,0,0.7-0.3,0.6-0.7l-0.2-4.1
|
||||||
|
c0-0.4-0.3-0.6-0.7-0.6h-1.3c-0.2,0-0.4-0.1-0.5-0.3L58.1,17c-0.1-0.2-0.3-0.2-0.5-0.3h-1.7c-0.3,0-0.5,0.2-0.6,0.4l-1.1,2.6
|
||||||
|
c0,0.1-0.1,0.2-0.2,0.3l-3,2.7c-0.3,0.2-0.7,0.2-0.9,0l-2.8-3c-0.3-0.3-0.2-0.8,0.1-1l3.1-2.2c0.1-0.1,0.3-0.1,0.4-0.1h1.6
|
||||||
|
c0,0,0,0,0.1,0l2.9-0.2l0.1-0.8c0-0.1,0.1-0.2,0.1-0.3l1.5-2.1c0.1-0.1,0.2-0.2,0.4-0.3l0.8-0.2c0.2,0,0.4,0,0.6,0.1l0.6,0.5
|
||||||
|
c0.3,0.3,0.3,0.8,0,1l-0.9,0.7c-0.3,0.2-0.3,0.7,0,1l1.6,1.5c0.2,0.2,0.6,0.3,0.9,0l1-0.8c0.1-0.1,0.2-0.3,0.2-0.5v-0.7
|
||||||
|
c0-0.4,0.3-0.7,0.7-0.7h1.5v-0.7l-2.4-2.3c-0.1-0.1-0.3-0.2-0.5-0.2h-1.9v-0.5l2-1.3c2.8,0.8,5.6,1.9,8.1,3.3
|
||||||
|
c-0.1,0.1-0.1,0.1-0.2,0.2L69,13.4c-0.2,0.1-0.4,0.4-0.4,0.6l0.1,2.6c0,0.3,0.2,0.5,0.4,0.6l2.2,0.8c0.3,0.1,0.6,0,0.8-0.2l1.7-2.5
|
||||||
|
c5.1,3.5,9.3,8.1,12.4,13.4l-0.5,0.7c-0.1,0.1-0.1,0.3-0.1,0.4v2.3c0,0.4,0.3,0.8,0.8,0.8h1.4v0.8c0,0.3-0.2,0.6-0.4,0.7l-2.3,1
|
||||||
|
c-0.1,0.1-0.2,0.1-0.3,0.2l-4.4,5.8c-0.1,0.1-0.1,0.3-0.1,0.4v5.8c0,0.2,0.1,0.4,0.2,0.5l4.3,4.9c0.1,0.2,0.3,0.3,0.6,0.3h4.8
|
||||||
|
c0.1,0,0.2,0,0.3-0.1l1.3-0.7c-1.4,22-19.6,39.4-41.9,39.4c-13.5,0-25.4-6.3-33.1-16.2l0.2-0.1c0.2-0.1,0.4-0.4,0.4-0.6v-2.4
|
||||||
|
C17.5,72.5,17.5,72.3,17.4,72.2z M50,7.9c3.3,0,6.5,0.4,9.6,1.1l-0.2,0.4c-0.1,0.2-0.4,0.3-0.6,0.3l-2.1-0.2c-0.4,0-0.8,0.3-0.7,0.7
|
||||||
|
l0.1,1.2h-1L55,10.7l-4.9,0.1v1.1l0.9-0.1v1.3c0,0.4-0.3,0.7-0.6,0.7h-2.2v-0.5l0.9-0.3L49,11.4c0-0.3-0.3-0.6-0.7-0.6h-2.7
|
||||||
|
c-0.1,0-0.2,0-0.4-0.1l-1.3-0.8c-0.2-0.1-0.4-0.1-0.6-0.1l-3,1l0.3,0.6c0.1,0.2,0.3,0.4,0.6,0.4h1.5l-0.1,0.5L36,12.6l-4.6-0.4
|
||||||
|
C37,9.5,43.3,7.9,50,7.9z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
17
src/index.js
17
src/index.js
@ -15,10 +15,8 @@ import thunk from 'redux-thunk';
|
|||||||
import { Router, browserHistory } from 'react-router';
|
import { Router, browserHistory } from 'react-router';
|
||||||
import { syncHistory, routeReducer } from 'react-router-redux';
|
import { syncHistory, routeReducer } from 'react-router-redux';
|
||||||
|
|
||||||
import { IntlProvider } from 'react-intl';
|
|
||||||
|
|
||||||
import { factory as userFactory } from 'components/user/factory';
|
import { factory as userFactory } from 'components/user/factory';
|
||||||
import i18n from 'services/i18n';
|
import { IntlProvider } from 'components/i18n';
|
||||||
import reducers from 'reducers';
|
import reducers from 'reducers';
|
||||||
import routesFactory from 'routes';
|
import routesFactory from 'routes';
|
||||||
|
|
||||||
@ -35,20 +33,15 @@ const store = applyMiddleware(
|
|||||||
)(createStore)(reducer);
|
)(createStore)(reducer);
|
||||||
|
|
||||||
userFactory(store)
|
userFactory(store)
|
||||||
.then(({lang}) =>
|
.then(() => {
|
||||||
i18n.require(
|
|
||||||
i18n.detectLanguage(lang)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(({locale, messages}) => {
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
|
<IntlProvider>
|
||||||
<Router history={browserHistory}>
|
<Router history={browserHistory}>
|
||||||
{routesFactory(store)}
|
{routesFactory(store)}
|
||||||
</Router>
|
</Router>
|
||||||
</ReduxProvider>
|
</IntlProvider>
|
||||||
</IntlProvider>,
|
</ReduxProvider>,
|
||||||
document.getElementById('app')
|
document.getElementById('app')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -36,4 +36,5 @@ p {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import auth from 'components/auth/reducer';
|
import auth from 'components/auth/reducer';
|
||||||
import user from 'components/user/reducer';
|
import user from 'components/user/reducer';
|
||||||
|
import i18n from 'components/i18n/reducer';
|
||||||
import popup from 'components/ui/popup/reducer';
|
import popup from 'components/ui/popup/reducer';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
auth,
|
auth,
|
||||||
user,
|
user,
|
||||||
|
i18n,
|
||||||
popup
|
popup
|
||||||
};
|
};
|
||||||
|
@ -25,5 +25,12 @@ export default {
|
|||||||
'/api/accounts/change-username',
|
'/api/accounts/change-username',
|
||||||
{username, password}
|
{username, password}
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
changeLang(lang) {
|
||||||
|
return request.post(
|
||||||
|
'/api/accounts/change-lang',
|
||||||
|
{lang}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,10 @@ import { addLocaleData } from 'react-intl';
|
|||||||
import enLocaleData from 'react-intl/locale-data/en';
|
import enLocaleData from 'react-intl/locale-data/en';
|
||||||
import ruLocaleData from 'react-intl/locale-data/ru';
|
import ruLocaleData from 'react-intl/locale-data/ru';
|
||||||
|
|
||||||
|
// till we have not so many locales, we can require their data at once
|
||||||
|
addLocaleData(enLocaleData);
|
||||||
|
addLocaleData(ruLocaleData);
|
||||||
|
|
||||||
const SUPPORTED_LANGUAGES = ['ru', 'en'];
|
const SUPPORTED_LANGUAGES = ['ru', 'en'];
|
||||||
const DEFAULT_LANGUAGE = 'en';
|
const DEFAULT_LANGUAGE = 'en';
|
||||||
function getUserLanguages(userSelectedLang = []) {
|
function getUserLanguages(userSelectedLang = []) {
|
||||||
@ -23,10 +27,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
require(locale) {
|
require(locale) {
|
||||||
// till we have not so many locales, we can require their data at once
|
|
||||||
addLocaleData(enLocaleData);
|
|
||||||
addLocaleData(ruLocaleData);
|
|
||||||
|
|
||||||
return new Promise(require(`bundle!i18n/${locale}.json`))
|
return new Promise(require(`bundle!i18n/${locale}.json`))
|
||||||
.then((messages) => ({locale, messages}));
|
.then((messages) => ({locale, messages}));
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ var webpackConfig = {
|
|||||||
loader: 'babel'
|
loader: 'babel'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png|gif|jpg)$/,
|
test: /\.(png|gif|jpg|svg)$/,
|
||||||
loader: 'url?limit=1000'
|
loader: 'url?limit=1000'
|
||||||
},
|
},
|
||||||
{ // TODO: увы, эта штука пока не работает. Хеш добавляется через ./webpack/node-sass-iconfont-importer
|
{ // TODO: увы, эта штука пока не работает. Хеш добавляется через ./webpack/node-sass-iconfont-importer
|
||||||
@ -197,9 +197,9 @@ var webpackConfig = {
|
|||||||
//
|
//
|
||||||
// Например:
|
// Например:
|
||||||
// file: components/ui/foo.scss
|
// file: components/ui/foo.scss
|
||||||
// images/foo.png -> components/ui/images/foo.png
|
// ./images/foo.png -> components/ui/images/foo.png
|
||||||
|
|
||||||
if (url[0] !== '/') {
|
if (url.indexOf('./') === 0) {
|
||||||
var relativeToRoot = dirname.split(rootPath + '/')[1];
|
var relativeToRoot = dirname.split(rootPath + '/')[1];
|
||||||
|
|
||||||
return path.join(relativeToRoot, url);
|
return path.join(relativeToRoot, url);
|
||||||
|
Reference in New Issue
Block a user