mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Extract general popups markup to its own component
Split popups controllers into separate components Implemented storybooks for all project's popups
This commit is contained in:
@@ -1,181 +1,37 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage as Message, injectIntl, IntlShape } from 'react-intl';
|
||||
import clsx from 'clsx';
|
||||
import { connect } from 'react-redux';
|
||||
import { changeLang } from 'app/components/user/actions';
|
||||
import LANGS from 'app/i18n';
|
||||
import formStyles from 'app/components/ui/form/form.scss';
|
||||
import popupStyles from 'app/components/ui/popup/popup.scss';
|
||||
import icons from 'app/components/ui/icons.scss';
|
||||
import React, { ComponentType, useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import styles from './languageSwitcher.scss';
|
||||
import LanguageList from './LanguageList';
|
||||
import LOCALES from 'app/i18n';
|
||||
import { changeLang } from 'app/components/user/actions';
|
||||
import { RootState } from 'app/reducers';
|
||||
|
||||
const translateUrl = 'http://ely.by/translate';
|
||||
import LanguageSwitcherPopup from './LanguageSwitcherPopup';
|
||||
|
||||
export interface LocaleData {
|
||||
code: string;
|
||||
name: string;
|
||||
englishName: string;
|
||||
progress: number;
|
||||
isReleased: boolean;
|
||||
}
|
||||
|
||||
export type LocalesMap = Record<string, LocaleData>;
|
||||
|
||||
type OwnProps = {
|
||||
onClose: () => void;
|
||||
langs: LocalesMap;
|
||||
emptyCaptions: Array<{
|
||||
src: string;
|
||||
caption: string;
|
||||
}>;
|
||||
type Props = {
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
interface Props extends OwnProps {
|
||||
intl: IntlShape;
|
||||
selectedLocale: string;
|
||||
changeLang: (lang: string) => void;
|
||||
}
|
||||
const LanguageSwitcher: ComponentType<Props> = ({ onClose = () => {} }) => {
|
||||
const selectedLocale = useSelector((state: RootState) => state.i18n.locale);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
class LanguageSwitcher extends React.Component<
|
||||
Props,
|
||||
{
|
||||
filter: string;
|
||||
filteredLangs: LocalesMap;
|
||||
}
|
||||
> {
|
||||
state = {
|
||||
filter: '',
|
||||
filteredLangs: this.props.langs,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
langs: LANGS,
|
||||
onClose() {},
|
||||
};
|
||||
|
||||
render() {
|
||||
const { selectedLocale, onClose, intl } = this.props;
|
||||
const { filteredLangs } = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.languageSwitcher}
|
||||
data-testid="language-switcher"
|
||||
data-e2e-active-locale={selectedLocale}
|
||||
>
|
||||
<div className={popupStyles.popup}>
|
||||
<div className={popupStyles.header}>
|
||||
<h2 className={popupStyles.headerTitle}>
|
||||
<Message key="siteLanguage" defaultMessage="Site language" />
|
||||
</h2>
|
||||
<span className={clsx(icons.close, popupStyles.close)} onClick={onClose} />
|
||||
</div>
|
||||
|
||||
<div className={styles.languageSwitcherBody}>
|
||||
<div className={styles.searchBox}>
|
||||
<input
|
||||
className={clsx(formStyles.lightTextField, formStyles.greenTextField)}
|
||||
placeholder={intl.formatMessage({
|
||||
key: 'startTyping',
|
||||
defaultMessage: 'Start typing…',
|
||||
})}
|
||||
onChange={this.onFilterUpdate}
|
||||
onKeyPress={this.onFilterKeyPress()}
|
||||
autoFocus
|
||||
/>
|
||||
<span className={styles.searchIcon} />
|
||||
</div>
|
||||
|
||||
<LanguageList
|
||||
selectedLocale={selectedLocale}
|
||||
langs={filteredLangs}
|
||||
onChangeLang={this.onChangeLang}
|
||||
/>
|
||||
|
||||
<div className={styles.improveTranslates}>
|
||||
<div className={styles.improveTranslatesIcon} />
|
||||
<div className={styles.improveTranslatesContent}>
|
||||
<div className={styles.improveTranslatesTitle}>
|
||||
<Message key="improveTranslates" defaultMessage="Improve Ely.by translation" />
|
||||
</div>
|
||||
<div className={styles.improveTranslatesText}>
|
||||
<Message
|
||||
key="improveTranslatesDescription"
|
||||
defaultMessage="Ely.by’s localization is a community effort. If you want to improve the translation of Ely.by, we'd love your help."
|
||||
/>{' '}
|
||||
<a href={translateUrl} target="_blank">
|
||||
<Message
|
||||
key="improveTranslatesParticipate"
|
||||
defaultMessage="Click here to participate."
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onChangeLang = this.changeLang.bind(this);
|
||||
|
||||
changeLang(lang: string) {
|
||||
this.props.changeLang(lang);
|
||||
|
||||
setTimeout(this.props.onClose, 300);
|
||||
}
|
||||
|
||||
onFilterUpdate = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const filter = event.currentTarget.value.trim().toLowerCase();
|
||||
const { langs } = this.props;
|
||||
|
||||
const result = Object.keys(langs).reduce((previous, key) => {
|
||||
if (
|
||||
langs[key].englishName.toLowerCase().indexOf(filter) === -1 &&
|
||||
langs[key].name.toLowerCase().indexOf(filter) === -1
|
||||
) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
previous[key] = langs[key];
|
||||
|
||||
return previous;
|
||||
}, {} as typeof langs);
|
||||
|
||||
this.setState({
|
||||
filter,
|
||||
filteredLangs: result,
|
||||
});
|
||||
};
|
||||
|
||||
onFilterKeyPress() {
|
||||
return (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key !== 'Enter' || this.state.filter === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const locales = Object.keys(this.state.filteredLangs);
|
||||
|
||||
if (locales.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.changeLang(locales[0]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(
|
||||
connect(
|
||||
(state: RootState) => ({
|
||||
selectedLocale: state.i18n.locale,
|
||||
}),
|
||||
{
|
||||
changeLang,
|
||||
const onChangeLang = useCallback(
|
||||
(lang: string) => {
|
||||
dispatch(changeLang(lang));
|
||||
// TODO: await language change and close popup, but not earlier than after 300ms
|
||||
setTimeout(onClose, 300);
|
||||
},
|
||||
)(LanguageSwitcher),
|
||||
);
|
||||
[dispatch, onClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<LanguageSwitcherPopup
|
||||
locales={LOCALES}
|
||||
activeLocale={selectedLocale}
|
||||
onSelect={onChangeLang}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSwitcher;
|
||||
|
@@ -0,0 +1,76 @@
|
||||
import React, { useState } from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import LanguageSwitcherPopup from './LanguageSwitcherPopup';
|
||||
|
||||
storiesOf('Components/Popups', module).add('LanguageSwitcherPopup', () => {
|
||||
const [activeLocale, setActiveLocale] = useState('en');
|
||||
|
||||
return (
|
||||
<LanguageSwitcherPopup
|
||||
locales={{
|
||||
// Released and completely translated language
|
||||
be: {
|
||||
code: 'be',
|
||||
name: 'Беларуская',
|
||||
englishName: 'Belarusian',
|
||||
progress: 100,
|
||||
isReleased: true,
|
||||
},
|
||||
// Not released, but completely translated language
|
||||
cs: {
|
||||
code: 'cs',
|
||||
name: 'Čeština',
|
||||
englishName: 'Czech',
|
||||
progress: 100,
|
||||
isReleased: false,
|
||||
},
|
||||
// English (:
|
||||
en: {
|
||||
code: 'en',
|
||||
name: 'English',
|
||||
englishName: 'English',
|
||||
progress: 100,
|
||||
isReleased: true,
|
||||
},
|
||||
// Released, but not completely translated language
|
||||
id: {
|
||||
code: 'id',
|
||||
name: 'Bahasa Indonesia',
|
||||
englishName: 'Indonesian',
|
||||
progress: 97,
|
||||
isReleased: true,
|
||||
},
|
||||
// A few more languages just to create a scroll and to test some interesting characters
|
||||
pt: {
|
||||
code: 'pt',
|
||||
name: 'Português do Brasil',
|
||||
englishName: 'Portuguese, Brazilian',
|
||||
progress: 99,
|
||||
isReleased: true,
|
||||
},
|
||||
vi: {
|
||||
code: 'vi',
|
||||
name: 'Tiếng Việt',
|
||||
englishName: 'Vietnamese',
|
||||
progress: 99,
|
||||
isReleased: true,
|
||||
},
|
||||
zh: {
|
||||
code: 'zh',
|
||||
name: '简体中文',
|
||||
englishName: 'Simplified Chinese',
|
||||
progress: 99,
|
||||
isReleased: true,
|
||||
},
|
||||
}}
|
||||
activeLocale={activeLocale}
|
||||
onSelect={(locale) => {
|
||||
action('onSelect')(locale);
|
||||
setActiveLocale(locale);
|
||||
}}
|
||||
onClose={action('onClose')}
|
||||
/>
|
||||
);
|
||||
});
|
@@ -0,0 +1,114 @@
|
||||
import React, { ChangeEventHandler, ComponentType, KeyboardEventHandler, useCallback, useState } from 'react';
|
||||
import { FormattedMessage as Message, useIntl } from 'react-intl';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import formStyles from 'app/components/ui/form/form.scss';
|
||||
import Popup from 'app/components/ui/popup';
|
||||
|
||||
import styles from './languageSwitcher.scss';
|
||||
import LanguagesList from './LanguagesList';
|
||||
|
||||
const translateUrl = 'http://ely.by/translate';
|
||||
|
||||
export interface LocaleData {
|
||||
code: string;
|
||||
name: string;
|
||||
englishName: string;
|
||||
progress: number;
|
||||
isReleased: boolean;
|
||||
}
|
||||
|
||||
export type LocalesMap = Record<string, LocaleData>;
|
||||
|
||||
interface Props {
|
||||
locales: LocalesMap;
|
||||
activeLocale: string;
|
||||
onSelect?: (lang: string) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const LanguageSwitcherPopup: ComponentType<Props> = ({ locales, activeLocale, onSelect, onClose }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
const filteredLocales = Object.keys(locales).reduce((acc, key) => {
|
||||
if (
|
||||
locales[key].englishName.toLowerCase().includes(filter) ||
|
||||
locales[key].name.toLowerCase().includes(filter)
|
||||
) {
|
||||
acc[key] = locales[key];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as typeof locales);
|
||||
|
||||
const onFilterChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||
(event) => {
|
||||
setFilter(event.currentTarget.value.trim().toLowerCase());
|
||||
},
|
||||
[setFilter],
|
||||
);
|
||||
const onFilterKeyPress = useCallback<KeyboardEventHandler<HTMLInputElement>>(
|
||||
(event) => {
|
||||
if (event.key !== 'Enter' || filter === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const localesKeys = Object.keys(filteredLocales);
|
||||
|
||||
if (localesKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect && onSelect(localesKeys[0]);
|
||||
},
|
||||
[filter, filteredLocales, onSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
title={<Message key="siteLanguage" defaultMessage="Site language" />}
|
||||
wrapperClassName={styles.boundings}
|
||||
bodyClassName={styles.body}
|
||||
onClose={onClose}
|
||||
data-testid="language-switcher"
|
||||
data-e2e-active-locale={activeLocale}
|
||||
>
|
||||
<div className={styles.searchBox}>
|
||||
<input
|
||||
className={clsx(formStyles.lightTextField, formStyles.greenTextField)}
|
||||
placeholder={intl.formatMessage({
|
||||
key: 'startTyping',
|
||||
defaultMessage: 'Start typing…',
|
||||
})}
|
||||
onChange={onFilterChange}
|
||||
onKeyPress={onFilterKeyPress}
|
||||
autoFocus
|
||||
/>
|
||||
<span className={styles.searchIcon} />
|
||||
</div>
|
||||
|
||||
<LanguagesList selectedLocale={activeLocale} locales={filteredLocales} onChangeLang={onSelect} />
|
||||
|
||||
<div className={styles.improveTranslates}>
|
||||
<div className={styles.improveTranslatesIcon} />
|
||||
<div className={styles.improveTranslatesContent}>
|
||||
<div className={styles.improveTranslatesTitle}>
|
||||
<Message key="improveTranslates" defaultMessage="Improve Ely.by translation" />
|
||||
</div>
|
||||
<div className={styles.improveTranslatesText}>
|
||||
<Message
|
||||
key="improveTranslatesDescription"
|
||||
defaultMessage="Ely.by’s localization is a community effort. If you want to improve the translation of Ely.by, we'd love your help."
|
||||
/>{' '}
|
||||
<a href={translateUrl} target="_blank">
|
||||
<Message key="improveTranslatesParticipate" defaultMessage="Click here to participate." />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSwitcherPopup;
|
@@ -12,7 +12,7 @@ import { FormattedMessage as Message } from 'react-intl';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import LocaleItem from './LocaleItem';
|
||||
import { LocalesMap } from './LanguageSwitcher';
|
||||
import { LocalesMap } from './LanguageSwitcherPopup';
|
||||
import styles from './languageSwitcher.scss';
|
||||
|
||||
import thatFuckingPumpkin from './images/that_fucking_pumpkin.svg';
|
||||
@@ -50,17 +50,21 @@ const emptyCaptions: ReadonlyArray<EmptyCaption> = [
|
||||
|
||||
const itemHeight = 51;
|
||||
|
||||
export default class LanguageList extends React.Component<{
|
||||
export default class LanguagesList extends React.Component<{
|
||||
locales: LocalesMap;
|
||||
selectedLocale: string;
|
||||
langs: LocalesMap;
|
||||
onChangeLang: (lang: string) => void;
|
||||
onChangeLang?: (lang: string) => void;
|
||||
}> {
|
||||
emptyListStateInner: HTMLDivElement | null;
|
||||
|
||||
static defaultProps = {
|
||||
onChangeLang: (): void => {},
|
||||
};
|
||||
|
||||
render() {
|
||||
const { selectedLocale, langs } = this.props;
|
||||
const isListEmpty = Object.keys(langs).length === 0;
|
||||
const firstLocale = Object.keys(langs)[0] || null;
|
||||
const { selectedLocale, locales } = this.props;
|
||||
const isListEmpty = Object.keys(locales).length === 0;
|
||||
const firstLocale = Object.keys(locales)[0] || null;
|
||||
const emptyCaption = this.getEmptyCaption();
|
||||
|
||||
return (
|
||||
@@ -71,7 +75,7 @@ export default class LanguageList extends React.Component<{
|
||||
willEnter={this.willEnter}
|
||||
>
|
||||
{(items) => (
|
||||
<div className={styles.languagesList} data-testid="language-list">
|
||||
<div className={styles.languagesList} data-testid="languages-list-item">
|
||||
<div
|
||||
className={clsx(styles.emptyLanguagesListWrapper, {
|
||||
[styles.emptyLanguagesListVisible]: isListEmpty,
|
||||
@@ -126,17 +130,18 @@ export default class LanguageList extends React.Component<{
|
||||
return (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// @ts-ignore has defaultProps value
|
||||
this.props.onChangeLang(lang);
|
||||
};
|
||||
}
|
||||
|
||||
getItemsWithDefaultStyles = (): Array<TransitionPlainStyle> => {
|
||||
return Object.keys({ ...this.props.langs }).reduce(
|
||||
return Object.keys({ ...this.props.locales }).reduce(
|
||||
(previous, key) => [
|
||||
...previous,
|
||||
{
|
||||
key,
|
||||
data: this.props.langs[key],
|
||||
data: this.props.locales[key],
|
||||
style: {
|
||||
height: itemHeight,
|
||||
opacity: 1,
|
||||
@@ -148,12 +153,12 @@ export default class LanguageList extends React.Component<{
|
||||
};
|
||||
|
||||
getItemsWithStyles = (): Array<TransitionStyle> => {
|
||||
return Object.keys({ ...this.props.langs }).reduce(
|
||||
return Object.keys({ ...this.props.locales }).reduce(
|
||||
(previous, key) => [
|
||||
...previous,
|
||||
{
|
||||
key,
|
||||
data: this.props.langs[key],
|
||||
data: this.props.locales[key],
|
||||
style: {
|
||||
height: spring(itemHeight, presets.gentle),
|
||||
opacity: spring(1, presets.gentle),
|
@@ -3,7 +3,7 @@ import { localeFlags } from 'app/components/i18n';
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import styles from './languageSwitcher.scss';
|
||||
import { LocaleData } from './LanguageSwitcher';
|
||||
import { LocaleData } from './LanguageSwitcherPopup';
|
||||
|
||||
interface Props {
|
||||
locale: LocaleData;
|
||||
|
@@ -8,17 +8,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.languageSwitcher {
|
||||
composes: popupWrapper from '~app/components/ui/popup/popup.scss';
|
||||
|
||||
.boundings {
|
||||
@include popupBounding(400px);
|
||||
}
|
||||
|
||||
.languageSwitcherBody {
|
||||
composes: body from '~app/components/ui/popup/popup.scss';
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $popupPadding;
|
||||
max-height: calc(100vh - 132px);
|
||||
|
||||
@media screen and (min-height: 630px) {
|
||||
|
Reference in New Issue
Block a user