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 { Button } from 'components/ui/form';
|
||||
import { LangMenu } from 'components/langMenu';
|
||||
|
||||
import styles from './appInfo.scss';
|
||||
import messages from './AppInfo.intl.json';
|
||||
@@ -36,6 +37,10 @@ export default class AppInfo extends Component {
|
||||
<div className={styles.goToAuth}>
|
||||
<Button onClick={onGoToAuth} label={messages.goToAuth} />
|
||||
</div>
|
||||
|
||||
<div className={styles.langMenu}>
|
||||
<LangMenu />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,3 +50,10 @@
|
||||
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 { userShape } from 'components/user/User';
|
||||
import { LangMenu } from 'components/langMenu';
|
||||
import langMenuMessages from 'components/langMenu/langMenu.intl.json';
|
||||
|
||||
import ProfileField from './ProfileField';
|
||||
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
|
||||
label={<Message {...messages.twoFactorAuth} />}
|
||||
value={<Message {...messages.disabled} />}
|
||||
@@ -82,7 +90,6 @@ export default class Profile extends Component {
|
||||
<ProfileField
|
||||
label={'UUID'}
|
||||
value={user.uuid}
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,23 @@ export default class ProfileField extends Component {
|
||||
static propTypes = {
|
||||
label: React.PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
||||
link: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: React.PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
||||
warningMessage: React.PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
readonly: PropTypes.bool
|
||||
warningMessage: React.PropTypes.oneOfType([PropTypes.string, PropTypes.element])
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className={styles.paramItem}>
|
||||
@@ -23,13 +33,11 @@ export default class ProfileField extends Component {
|
||||
<div className={styles.paramName}>{label}:</div>
|
||||
<div className={styles.paramValue}>{value}</div>
|
||||
|
||||
{readonly ? '' : (
|
||||
<div className={styles.paramAction}>
|
||||
<Link to={link}>
|
||||
<span className={styles.paramEditIcon} />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{Action ? (
|
||||
<Action to={link} className={styles.paramAction}>
|
||||
<span className={styles.paramEditIcon} />
|
||||
</Action>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{warningMessage ? (
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
|
||||
.paramAction {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.paramEditIcon {
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
}
|
||||
|
||||
[type="submit"] {
|
||||
background: url('images/loader_button.gif') #95a5a6 center center;
|
||||
background: url('./images/loader_button.gif') #95a5a6 center center;
|
||||
|
||||
cursor: default;
|
||||
color: #fff;
|
||||
|
||||
@@ -8,3 +8,13 @@
|
||||
|
||||
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 accounts from 'services/api/accounts';
|
||||
import { setLocale } from 'components/i18n/actions';
|
||||
|
||||
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 function setUser(payload) {
|
||||
return {
|
||||
@@ -26,6 +46,7 @@ export function setUser(payload) {
|
||||
export function logout() {
|
||||
return (dispatch) => {
|
||||
dispatch(setUser({isGuest: true}));
|
||||
dispatch(changeLang());
|
||||
dispatch(routeActions.push('/login'));
|
||||
};
|
||||
}
|
||||
@@ -35,6 +56,8 @@ export function fetchUserData() {
|
||||
accounts.current()
|
||||
.then((resp) => {
|
||||
dispatch(updateUser(resp));
|
||||
|
||||
return dispatch(changeLang(resp.lang));
|
||||
})
|
||||
.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
|
||||
@@ -8,15 +8,15 @@ import { authenticate } from 'components/user/actions';
|
||||
* @return {Promise} a promise, that resolves in User state
|
||||
*/
|
||||
export function factory(store) {
|
||||
const state = store.getState();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (state.user.token) {
|
||||
const {user} = store.getState();
|
||||
|
||||
if (user.token) {
|
||||
// authorizing user if it is possible
|
||||
store.dispatch(authenticate(state.user.token))
|
||||
.then(() => resolve(store.getState().user), reject);
|
||||
} else {
|
||||
resolve(state.user);
|
||||
return store.dispatch(authenticate(user.token)).then(resolve, reject);
|
||||
}
|
||||
|
||||
// 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';
|
||||
|
||||
@@ -8,6 +8,16 @@ export default function user(
|
||||
{type, payload = null}
|
||||
) {
|
||||
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:
|
||||
if (!payload) {
|
||||
throw new Error('payload is required for user reducer');
|
||||
|
||||
Reference in New Issue
Block a user