Create app namespace for all absolute requires of app modules. Move all packages under packages yarn workspace

This commit is contained in:
SleepWalker
2019-12-07 21:02:00 +02:00
parent d8d2df0702
commit f9d3bb4e20
404 changed files with 758 additions and 742 deletions

View File

@ -0,0 +1,156 @@
import React from 'react';
import { TransitionMotion, spring, presets } from 'react-motion';
import { FormattedMessage as Message } from 'react-intl';
import classNames from 'classnames';
import LocaleItem from './LocaleItem';
import messages from './languageSwitcher.intl.json';
import { LocalesMap } from './LanguageSwitcher';
import styles from './languageSwitcher.scss';
import thatFuckingPumpkin from './images/that_fucking_pumpkin.svg';
import mayTheForceBeWithYou from './images/may_the_force_be_with_you.svg';
import biteMyShinyMetalAss from './images/bite_my_shiny_metal_ass.svg';
import iTookAnArrowInMyKnee from './images/i_took_an_arrow_in_my_knee.svg';
const itemHeight = 51;
export default class LanguageList extends React.Component<{
selectedLocale: string;
langs: LocalesMap;
onChangeLang: (lang: string) => void;
}> {
emptyListStateInner: HTMLDivElement | null;
render() {
const { selectedLocale, langs } = this.props;
const isListEmpty = Object.keys(langs).length === 0;
const firstLocale = Object.keys(langs)[0] || null;
const emptyCaption = this.getEmptyCaption();
return (
<TransitionMotion
defaultStyles={this.getItemsWithDefaultStyles()}
styles={this.getItemsWithStyles()}
willLeave={this.willLeave}
willEnter={this.willEnter}
>
{items => (
<div className={styles.languagesList}>
<div
className={classNames(styles.emptyLanguagesListWrapper, {
[styles.emptyLanguagesListVisible]: isListEmpty,
})}
style={{
height:
isListEmpty && this.emptyListStateInner
? this.emptyListStateInner.clientHeight
: 0,
}}
>
<div
ref={(elem: HTMLDivElement | null) =>
(this.emptyListStateInner = elem)
}
className={styles.emptyLanguagesList}
>
<img
src={emptyCaption.src}
alt={emptyCaption.caption}
className={styles.emptyLanguagesListCaption}
/>
<div className={styles.emptyLanguagesListSubtitle}>
<Message {...messages.weDoNotSupportThisLang} />
</div>
</div>
</div>
{items.map(({ key: locale, data: definition, style }) => (
<div
key={locale}
style={style}
className={classNames(styles.languageItem, {
[styles.activeLanguageItem]: locale === selectedLocale,
[styles.firstLanguageItem]: locale === firstLocale,
})}
onClick={this.onChangeLang(locale)}
>
<LocaleItem locale={definition} />
</div>
))}
</div>
)}
</TransitionMotion>
);
}
getEmptyCaption() {
const emptyCaptions = [
{
// Homestuck
src: thatFuckingPumpkin,
caption: 'That fucking pumpkin',
},
{
// Star Wars
src: mayTheForceBeWithYou,
caption: 'May The Force Be With You',
},
{
// Futurama
src: biteMyShinyMetalAss,
caption: 'Bite my shiny metal ass',
},
{
// The Elder Scrolls V: Skyrim
src: iTookAnArrowInMyKnee,
caption: 'I took an arrow in my knee',
},
];
return emptyCaptions[Math.floor(Math.random() * emptyCaptions.length)];
}
onChangeLang(lang: string) {
return (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
this.props.onChangeLang(lang);
};
}
getItemsWithDefaultStyles = () =>
this.getItemsWithStyles({ useSpring: false });
getItemsWithStyles = (
{ useSpring }: { useSpring?: boolean } = { useSpring: true },
) =>
Object.keys({ ...this.props.langs }).reduce(
(previous, key) => [
...previous,
{
key,
data: this.props.langs[key],
style: {
height: useSpring ? spring(itemHeight, presets.gentle) : itemHeight,
opacity: useSpring ? spring(1, presets.gentle) : 1,
},
},
],
[],
);
willEnter() {
return {
height: 0,
opacity: 1,
};
}
willLeave() {
return {
height: spring(0),
opacity: spring(0),
};
}
}

View File

@ -0,0 +1,175 @@
import React from 'react';
import { FormattedMessage as Message, injectIntl, IntlShape } from 'react-intl';
import classNames from 'classnames';
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 styles from './languageSwitcher.scss';
import messages from './languageSwitcher.intl.json';
import LanguageList from './LanguageList';
import { RootState } from 'app/reducers';
const translateUrl = 'http://ely.by/translate';
export type LocaleData = {
code: string;
name: string;
englishName: string;
progress: number;
isReleased: boolean;
};
export type LocalesMap = { [code: string]: LocaleData };
type OwnProps = {
onClose: () => void;
langs: LocalesMap;
emptyCaptions: Array<{
src: string;
caption: string;
}>;
};
interface Props extends OwnProps {
intl: IntlShape;
selectedLocale: string;
changeLang: (lang: string) => void;
}
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}>
<div className={popupStyles.popup}>
<div className={popupStyles.header}>
<h2 className={popupStyles.headerTitle}>
<Message {...messages.siteLanguage} />
</h2>
<span
className={classNames(icons.close, popupStyles.close)}
onClick={onClose}
/>
</div>
<div className={styles.languageSwitcherBody}>
<div className={styles.searchBox}>
<input
className={classNames(
formStyles.lightTextField,
formStyles.greenTextField,
)}
placeholder={intl.formatMessage(messages.startTyping)}
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 {...messages.improveTranslates} />
</div>
<div className={styles.improveTranslatesText}>
<Message {...messages.improveTranslatesDescription} />{' '}
<a href={translateUrl} target="_blank">
<Message {...messages.improveTranslatesParticipate} />
</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;
}, {});
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,
},
)(LanguageSwitcher),
);

View File

@ -0,0 +1,44 @@
import React from 'react';
import { localeFlags } from 'app/components/i18n';
import { FormattedMessage as Message } from 'react-intl';
import messages from './languageSwitcher.intl.json';
import styles from './languageSwitcher.scss';
import { LocaleData } from './LanguageSwitcher';
export default function LocaleItem({ locale }: { locale: LocaleData }) {
const { code, name, englishName, progress, isReleased } = locale;
let progressLabel;
if (progress !== 100) {
progressLabel = (
<Message
{...messages.translationProgress}
values={{
progress,
}}
/>
);
} else if (!isReleased) {
progressLabel = <Message {...messages.mayBeInaccurate} />;
}
return (
<div className={styles.languageFlex}>
<div
className={styles.languageIco}
style={{
backgroundImage: `url('${localeFlags.getIconUrl(code)}')`,
}}
/>
<div className={styles.languageCaptions}>
<div className={styles.languageName}>{name}</div>
<div className={styles.languageSubName}>
{englishName} {progressLabel ? '| ' : ''} {progressLabel}
</div>
</div>
<span className={styles.languageCircle} />
</div>
);
}

View File

@ -0,0 +1,53 @@
import React from 'react';
import classNames from 'classnames';
import { localeFlags } from 'app/components/i18n';
import LANGS from 'app/i18n';
import { connect } from 'react-redux';
import { create as createPopup } from 'app/components/ui/popup/actions';
import LanguageSwitcher from 'app/components/languageSwitcher';
import { RootState } from 'app/reducers';
import styles from './link.scss';
type Props = {
userLang: string;
interfaceLocale: string;
showLanguageSwitcherPopup: (event: React.MouseEvent<HTMLSpanElement>) => void;
};
function LanguageLink({
userLang,
interfaceLocale,
showLanguageSwitcherPopup,
}: Props) {
const localeDefinition = LANGS[userLang] || LANGS[interfaceLocale];
return (
<span
className={classNames(styles.languageLink, {
[styles.mark]: userLang !== interfaceLocale,
})}
onClick={showLanguageSwitcherPopup}
>
<span
className={styles.languageIcon}
style={{
backgroundImage: `url('${localeFlags.getIconUrl(
localeDefinition.code,
)}')`,
}}
/>
{localeDefinition.name}
</span>
);
}
export default connect(
(state: RootState) => ({
userLang: state.user.lang,
interfaceLocale: state.i18n.locale,
}),
{
showLanguageSwitcherPopup: () => createPopup({ Popup: LanguageSwitcher }),
},
)(LanguageLink);

View File

@ -0,0 +1,35 @@
@import '~app/components/ui/colors.scss';
.languageLink {
position: relative;
color: #666;
border-bottom: 1px dotted #666;
text-decoration: none;
transition: 0.25s;
cursor: pointer;
&.mark {
&:after {
position: absolute;
content: '*';
color: $red;
font-size: 75%;
top: -0.2em;
}
}
}
.languageIcon {
$height: 13px;
position: relative;
top: 1px;
display: inline-block;
margin-right: 4px;
height: $height;
width: $height * 4 / 3;
box-shadow: 0 0 1px rgba(#000, 0.2);
background-size: cover;
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 237.77 14.18">
<path d="M4.48,0.84L8.29,15H0.38V13H6.3L3.55,2.79H0.38v-2h4.1Zm5.27,6.1V0.84h1.17V5h5.29l2.32-4.15H20L16.6,6.94H9.76Zm6.85,2L20,15H18.55l-2.32-4.15H10.93V15H9.76V8.9H16.6Zm6.87-6.1,2.7,10,3.22-12h1.46L27,15H25.29L21.48,0.84h4.39l1.32,4.88H25.73L24.93,2.79H23.47ZM43.39,12.4L46,7.68h1.46L43.39,15H42.21l-4.1-7.34h1.46l2.64,4.73V0.84h1.17V12.4Zm6.74-5.46H56v2H50.12v-2Zm7,6.1v2H49V13h8.2ZM49,2.79v-2h8.2v2H49Zm16.45,0L62.87,15H61.4L58.62,0.84h1.32L62.14,12l2.2-11.18H68v2H66.82V15H65.65V2.79H65.4ZM77.6,8.9H76.43V13h2.93V8.72A8.47,8.47,0,0,1,77.6,8.9ZM75.26,15V6.94H77.6a7.07,7.07,0,0,0,1.76-.21V0.84h1.17V6.32a9.08,9.08,0,0,0,4.1-4.5V4.26a8.1,8.1,0,0,1-4.1,4.1V13h4.1v2H75.26Zm15-12.21L87.5,13h8.61L93.36,2.79H90.26ZM86.68,15L86.1,13,89.32,0.84h5L97.52,13l-0.59,2H86.68ZM99,2.78V0.84h8.2v2l-5,12.21h-1.46l5-12.21H99Zm13.18,2.94v4.39H111V5.72h1.17Zm-2.34-4.88v9.28h-1.17V0.84h1.17Zm4.69,4.88V15h-1.17V5.72h1.17Zm8.24-2.93L120.23,15h-1.46L116,0.84h1.32L119.49,12l2.2-11.18h3.66v2h-1.17V15H123V2.79h-0.25Zm18.35,4.15v2h-6.74v-2h6.74Zm-5.79-4.15L134.08,5h-1.46L135,0.84h5.57L142.87,5H141.4l-1.24-2.2h-4.84ZM135,15l-2.34-4.15h1.46l1.24,2.2h4.84l1.24-2.2h1.46L140.52,15H135ZM151.6,2.79L149.07,15H147.6L144.82,0.84h1.32L148.34,12l2.2-11.18h3.66v2H153V15h-1.17V2.79H151.6Zm11.91,0q-0.88,0-.88,2.93v4.39q0,2.93.88,2.93h6.74q0.88,0,.88-2.93V5.72q0-2.93-.88-2.93h-6.74ZM162.92,15q-1.46-.49-1.46-4.88V5.72q0-4.39,1.46-4.88h7.91q1.46,0.49,1.46,4.88v4.39q0,4.39-1.46,4.88h-7.91ZM175.81,0.84H177V15h-1.17V3.28l-1.46,2-0.59-1.46Zm7.91,11.56,2.64-4.73h1.46L183.72,15h-1.17l-4.1-7.34h1.46l2.64,4.73V0.84h1.17V12.4Zm6.74-5.46h5.86v2h-5.86v-2Zm7,6.1v2h-8.2V13h8.2Zm-8.2-10.25v-2h8.2v2h-8.2Zm17.46,0,2.7,10,3.22-12h1.46L210.32,15h-1.76L204.75,0.84h4.39l1.32,4.88H209l-0.79-2.93h-1.46Zm13,0L217,13h8.61L222.86,2.79h-3.11ZM216.18,15l-0.59-2,3.22-12.21h5L227,13l-0.59,2H216.18Zm22-14.16V12.56A2.57,2.57,0,0,1,235.22,15h-3.81a2.57,2.57,0,0,1-2.93-2.44V0.84h1.17V11.58q0,1.46,1.76,1.46h3.81Q237,13,237,11.58V2.79H234.7L233.46,5H232l2.34-4.15h3.81Z" transform="translate(-0.38 -0.84)" fill="#444"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1,2 @@
export { default } from './LanguageSwitcher';
export { default as ChangeLanguageLink } from './changeLanguageLink/ChangeLanguageLink';

View File

@ -0,0 +1,10 @@
{
"siteLanguage": "Site language",
"startTyping": "Start typing…",
"translationProgress": "{progress}% translated",
"mayBeInaccurate": "May be inaccurate",
"weDoNotSupportThisLang": "Sorry, we do not support this language",
"improveTranslates": "Improve Ely.by translation",
"improveTranslatesDescription": "Ely.bys localization is a community effort. If you want to improve the translation of Ely.by, we'd love your help.",
"improveTranslatesParticipate": "Click here to participate."
}

View File

@ -0,0 +1,231 @@
@import '~app/components/ui/colors.scss';
@import '~app/components/ui/fonts.scss';
@import '~app/components/ui/popup/popup.scss';
@mixin hideFooter {
@media (max-height: 455px) {
@content;
}
}
.languageSwitcher {
composes: popupWrapper from '~app/components/ui/popup/popup.scss';
@include popupBounding(400px);
}
.languageSwitcherBody {
composes: body from '~app/components/ui/popup/popup.scss';
display: flex;
flex-direction: column;
max-height: calc(100vh - 132px);
@media screen and (min-height: 630px) {
max-height: 500px;
}
}
.searchBox {
position: relative;
margin-bottom: 20px;
}
.searchIcon {
composes: search from '~app/components/ui/icons.scss';
position: absolute;
top: 14px;
right: 12px;
font-size: 22px;
color: #edebe5;
pointer-events: none; // Иконка чисто декоративная, так что клик должен проходить сквозь неё
}
$languageListBorderColor: #eee;
$languageListBorderStyle: 1px solid $languageListBorderColor;
.languagesList {
flex-grow: 1;
overflow-y: auto;
border-top: $languageListBorderStyle;
border-bottom: $languageListBorderStyle;
margin-bottom: 20px;
&::-webkit-scrollbar {
width: 12px;
}
&::-webkit-scrollbar-track {
background-color: #f3f3f3;
}
&::-webkit-scrollbar-thumb {
background-color: #d8d5d0;
}
@include hideFooter {
& {
margin-bottom: 0;
}
}
}
.languageItem {
font-family: $font-family-title;
transition: background-color 0.25s;
cursor: pointer;
overflow: hidden;
line-height: 1;
&:hover {
background-color: $whiteButtonLight;
}
}
.languageFlex {
box-sizing: border-box;
display: flex;
align-items: center;
padding: 10px;
border-top: $languageListBorderStyle;
.firstLanguageItem & {
border-top: none;
}
}
.languageIco {
display: inline-block;
margin-right: 7px;
width: 40px;
height: 30px;
box-shadow: 0 0 1px rgba(#000, 0.2);
background: no-repeat;
background-size: cover;
}
.languageCaptions {
flex: 1;
overflow: hidden;
}
.languageName {
font-size: 15px;
margin-bottom: 2px;
}
.languageSubName {
font-size: 11px;
color: #ccc;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
// Реализация радио кнопки. Когда у нас будет нормальный компонент радио кнопок, нужно будет перейти на него
.languageCircle {
composes: checkmark from '~app/components/ui/icons.scss';
position: relative;
box-sizing: border-box;
width: 22px;
height: 22px;
right: -32px;
font-size: 10px;
line-height: 18px;
text-align: center;
color: #fff;
border: 2px solid #dcd8cd;
border-radius: 50%;
transition: 0.5s cubic-bezier(0.19, 1, 0.22, 1);
&:before {
opacity: 0;
transition: opacity 0.3s;
}
.languageItem:hover & {
right: 0;
}
.activeLanguageItem & {
border-color: $green;
background: $green;
right: 0;
&:before {
opacity: 1;
}
}
}
.emptyLanguagesListWrapper {
$transitionTime: 0.5s;
opacity: 0;
overflow: hidden;
transition: height $transitionTime;
&.emptyLanguagesListVisible {
opacity: 1;
transition: $transitionTime;
}
}
.emptyLanguagesList {
padding: 20px;
text-align: center;
}
.emptyLanguagesListCaption {
height: 20px;
max-width: 100%;
margin-bottom: 5px;
}
.emptyLanguagesListSubtitle {
font-family: $font-family-title;
color: #ccc;
font-size: 13px;
}
.improveTranslates {
border: 1px solid #dedede;
background: #f3f1ed;
padding: 10px;
display: flex;
flex-shrink: 0;
@include hideFooter {
& {
display: none;
}
}
}
.improveTranslatesIcon {
composes: translate from '~app/components/ui/icons.scss';
color: lighter($blue);
font-size: 22px;
margin-right: 10px;
}
.improveTranslatesContent {
}
.improveTranslatesTitle {
font-family: $font-family-title;
font-size: 13px;
margin-bottom: 3px;
}
.improveTranslatesText {
font-size: 10px;
color: #9a9a9a;
line-height: 1.2;
}