From 7dbd569d453ab4de75174f2ab405477e784a388a Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Sat, 30 Dec 2017 23:38:54 +0200 Subject: [PATCH] #379: split languageSwitcher to smaller components --- scripts/i18n-onesky.js | 3 +- src/components/footerMenu/FooterMenu.js | 10 +- .../languageSwitcher/LanguageList.js | 148 ++++++++++++ .../languageSwitcher/LanguageSwitcher.js | 222 +++--------------- src/components/languageSwitcher/LocaleItem.js | 47 ++++ .../changeLanguageLink/ChangeLanguageLink.js | 31 +++ .../changeLanguageLink/link.scss | 21 ++ src/components/languageSwitcher/index.js | 3 + .../languageSwitcher/languageSwitcher.scss | 1 + src/components/profile/Profile.js | 57 ++--- src/components/profile/profile.scss | 22 -- src/components/user/reducer.js | 4 +- src/i18n/index.json | 12 + webpack.config.js | 2 +- 14 files changed, 330 insertions(+), 253 deletions(-) create mode 100644 src/components/languageSwitcher/LanguageList.js create mode 100644 src/components/languageSwitcher/LocaleItem.js create mode 100644 src/components/languageSwitcher/changeLanguageLink/ChangeLanguageLink.js create mode 100644 src/components/languageSwitcher/changeLanguageLink/link.scss create mode 100644 src/components/languageSwitcher/index.js diff --git a/scripts/i18n-onesky.js b/scripts/i18n-onesky.js index cf6847d..8463c55 100644 --- a/scripts/i18n-onesky.js +++ b/scripts/i18n-onesky.js @@ -109,7 +109,7 @@ function sortByKeys(object) { * @return {string} */ function trimValueInBrackets(value) { - return value.match(/^([^\(]+)/)[0].trim(); + return value.match(/^([^(]+)/)[0].trim(); } async function pullReadyLanguages() { @@ -141,6 +141,7 @@ async function pull() { const mapFileContent = {}; langs.map((elem) => { mapFileContent[elem.locale] = { + code: elem.locale, name: ORIGINAL_NAMES_MAP[elem.locale] || trimValueInBrackets(elem.local_name), englishName: ENGLISH_NAMES_MAP[elem.locale] || trimValueInBrackets(elem.english_name), progress: parseFloat(elem.translation_progress), diff --git a/src/components/footerMenu/FooterMenu.js b/src/components/footerMenu/FooterMenu.js index 9a3bfa2..35f7b57 100644 --- a/src/components/footerMenu/FooterMenu.js +++ b/src/components/footerMenu/FooterMenu.js @@ -1,8 +1,11 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; - +import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { FormattedMessage as Message } from 'react-intl'; +import ContactForm from 'components/contact/ContactForm'; +import LanguageSwitcher from 'components/languageSwitcher'; +import { create as createPopup } from 'components/ui/popup/actions'; import styles from './footerMenu.scss'; import messages from './footerMenu.intl.json'; @@ -47,11 +50,6 @@ class FooterMenu extends Component { }; } -import { connect } from 'react-redux'; -import ContactForm from 'components/contact/ContactForm'; -import LanguageSwitcher from 'components/languageSwitcher/LanguageSwitcher'; -import { create as createPopup } from 'components/ui/popup/actions'; - // mark this component, as not pure, because it is stateless, // but should be re-rendered, if current lang was changed export default connect(null, { diff --git a/src/components/languageSwitcher/LanguageList.js b/src/components/languageSwitcher/LanguageList.js new file mode 100644 index 0000000..91c7917 --- /dev/null +++ b/src/components/languageSwitcher/LanguageList.js @@ -0,0 +1,148 @@ +// @flow + +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 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'; + +import type { LocalesMap } from './LanguageSwitcher'; + +const itemHeight = 51; + +export default class LanguageList extends React.Component<{ + userLang: string, + langs: LocalesMap, + onChangeLang: (lang: string) => void, +}> { + emptyListStateInner: ?HTMLDivElement; + + render() { + const { userLang, langs } = this.props; + const isListEmpty = Object.keys(langs).length === 0; + const firstLocale = Object.keys(langs)[0] || null; + const emptyCaption = this.getEmptyCaption(); + + return ( + + {(items) => ( +
+
+
this.emptyListStateInner = elem} + className={styles.emptyLanguagesList} + > + {emptyCaption.caption} +
+ +
+
+
+ + {items.map(({key: locale, data: definition, style}) => ( +
+ +
+ ))} +
+ )} +
+ ); + } + + 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: SyntheticMouseEvent) => { + event.preventDefault(); + + this.props.onChangeLang(lang); + }; + } + + getItemsWithDefaultStyles = () => this.getItemsWithStyles({ useSpring: false }); + + getItemsWithStyles = ({ useSpring }: { useSpring?: bool } = { 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), + }; + } +} diff --git a/src/components/languageSwitcher/LanguageSwitcher.js b/src/components/languageSwitcher/LanguageSwitcher.js index 1490c48..7024fab 100644 --- a/src/components/languageSwitcher/LanguageSwitcher.js +++ b/src/components/languageSwitcher/LanguageSwitcher.js @@ -1,46 +1,41 @@ +// @flow import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { TransitionMotion, spring, presets } from 'react-motion'; import { FormattedMessage as Message, intlShape } from 'react-intl'; - import classNames from 'classnames'; -import { localeFlags } from 'components/i18n'; import LANGS from 'i18n/index.json'; - import formStyles from 'components/ui/form/form.scss'; import popupStyles from 'components/ui/popup/popup.scss'; import icons from 'components/ui/icons.scss'; import styles from './languageSwitcher.scss'; import messages from './languageSwitcher.intl.json'; - -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'; +import LanguageList from './LanguageList'; const improveTranslationUrl = 'http://ely.by/erickskrauch/posts/174943'; -const itemHeight = 51; -class LanguageSwitcher extends Component { - static displayName = 'LanguageSwitcher'; +export type LocaleData = { + code: string, + name: string, + englishName: string, + progress: number, + isReleased: bool, +}; - static propTypes = { - onClose: PropTypes.func, - userLang: PropTypes.string, - changeLang: PropTypes.func, - langs: PropTypes.objectOf(PropTypes.shape({ - name: PropTypes.string.isRequired, - englishName: PropTypes.string.isRequired, - progress: PropTypes.number.isRequired, - isReleased: PropTypes.bool.isRequired, - })).isRequired, - emptyCaptions: PropTypes.arrayOf(PropTypes.shape({ - src: PropTypes.string.isRequired, - caption: PropTypes.string.isRequired, - })).isRequired, - }; +export type LocalesMap = {[code: string]: LocaleData}; +class LanguageSwitcher extends Component<{ + onClose: Function, + userLang: string, + changeLang: (lang: string) => void, + langs: LocalesMap, + emptyCaptions: Array<{ + src: string, + caption: string, + }>, +}, { + filter: string, + filteredLangs: LocalesMap, +}> { static contextTypes = { intl: intlShape.isRequired, }; @@ -53,35 +48,11 @@ class LanguageSwitcher extends Component { static defaultProps = { langs: LANGS, onClose() {}, - 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', - }, - ], }; render() { - const {userLang, onClose, emptyCaptions} = this.props; - const isListEmpty = Object.keys(this.state.filteredLangs).length === 0; - const firstLocale = Object.keys(this.state.filteredLangs)[0] || null; - const emptyCaption = emptyCaptions[Math.floor(Math.random() * emptyCaptions.length)]; + const { userLang, onClose } = this.props; + const { filteredLangs } = this.state; return (
@@ -108,53 +79,11 @@ class LanguageSwitcher extends Component {
- - {(items) => ( -
-
-
this.emptyListStateInner = elem} - className={styles.emptyLanguagesList} - > - {emptyCaption.caption} -
- -
-
-
- - {items.map(({key: locale, data: definition, style}) => ( -
  • - {this.renderLanguageItem(locale, definition)} -
  • - ))} -
    - )} -
    +
    @@ -179,57 +108,21 @@ class LanguageSwitcher extends Component { ); } - renderLanguageItem(locale, localeData) { - const {name, progress, isReleased} = localeData; - let progressLabel; - if (progress !== 100) { - progressLabel = ( - - ); - } else if (!isReleased) { - progressLabel = ( - - ); - } - - return ( -
    -
    -
    -
    - {name} -
    -
    - {localeData.englishName} {progressLabel ? '| ' : ''} {progressLabel} -
    -
    - -
    - ); - } - - onChangeLang(lang) { - return (event) => { - event.preventDefault(); - this.changeLang(lang); - }; - } + onChangeLang = this.changeLang.bind(this); changeLang(lang) { this.props.changeLang(lang); + setTimeout(this.props.onClose, 300); } - onFilterUpdate = (event) => { - const filter = event.target.value.trim().toLowerCase(); + onFilterUpdate = (event: SyntheticInputEvent) => { + const filter = event.currentTarget.value.trim().toLowerCase(); const { langs } = this.props; + const result = Object.keys(langs).reduce((previous, key) => { if (langs[key].englishName.toLowerCase().search(filter) === -1 - && langs[key].name.toLowerCase().search(filter) === -1 + && langs[key].name.toLowerCase().search(filter) === -1 ) { return previous; } @@ -246,12 +139,13 @@ class LanguageSwitcher extends Component { }; onFilterKeyPress() { - return (event) => { + return (event: SyntheticInputEvent) => { if (event.key !== 'Enter' || this.state.filter === '') { return; } const locales = Object.keys(this.state.filteredLangs); + if (locales.length === 0) { return; } @@ -259,46 +153,6 @@ class LanguageSwitcher extends Component { this.changeLang(locales[0]); }; } - - getItemsWithDefaultStyles = () => Object.keys(this.props.langs).reduce((previous, key) => { - return [ - ...previous, - { - key, - data: this.props.langs[key], - style: { - height: itemHeight, - opacity: 1, - }, - }, - ]; - }, {}); - - getItemsWithStyles = () => Object.keys({...this.state.filteredLangs}).reduce((previous, key) => [ - ...previous, - { - key, - data: this.props.langs[key], - style: { - height: spring(itemHeight, presets.gentle), - opacity: spring(1, presets.gentle), - }, - }, - ], []); - - willEnter() { - return { - height: 0, - opacity: 1, - }; - } - - willLeave() { - return { - height: spring(0), - opacity: spring(0), - }; - } } import { connect } from 'react-redux'; diff --git a/src/components/languageSwitcher/LocaleItem.js b/src/components/languageSwitcher/LocaleItem.js new file mode 100644 index 0000000..d0170ef --- /dev/null +++ b/src/components/languageSwitcher/LocaleItem.js @@ -0,0 +1,47 @@ +// @flow +import React from 'react'; + +import { localeFlags } from 'components/i18n'; +import { FormattedMessage as Message } from 'react-intl'; +import messages from './languageSwitcher.intl.json'; +import styles from './languageSwitcher.scss'; +import type { LocaleData } from './LanguageSwitcher'; + +export default function LocaleItem({ + locale, +}: { + locale: LocaleData, +}) { + const {code, name, englishName, progress, isReleased} = locale; + + let progressLabel; + + if (progress !== 100) { + progressLabel = ( + + ); + } else if (!isReleased) { + progressLabel = ( + + ); + } + + return ( +
    +
    +
    +
    + {name} +
    +
    + {englishName} {progressLabel ? '| ' : ''} {progressLabel} +
    +
    + +
    + ); +} diff --git a/src/components/languageSwitcher/changeLanguageLink/ChangeLanguageLink.js b/src/components/languageSwitcher/changeLanguageLink/ChangeLanguageLink.js new file mode 100644 index 0000000..547792e --- /dev/null +++ b/src/components/languageSwitcher/changeLanguageLink/ChangeLanguageLink.js @@ -0,0 +1,31 @@ +// @flow +import React from 'react'; +import { connect } from 'react-redux'; +import { create as createPopup } from 'components/ui/popup/actions'; +import { localeFlags } from 'components/i18n'; +import LANGS from 'i18n/index.json'; +import LanguageSwitcher from '../LanguageSwitcher'; +import styles from './link.scss'; + +function LanguageLink({ + userLang, + showLanguageSwitcherPopup, +}: { + userLang: string, + showLanguageSwitcherPopup: Function, +}) { + return ( + + + {LANGS[userLang].name} + + ); +} + +export default connect((state) => ({ + userLang: state.user.lang, +}), { + showLanguageSwitcherPopup: () => createPopup(LanguageSwitcher), +})(LanguageLink); diff --git a/src/components/languageSwitcher/changeLanguageLink/link.scss b/src/components/languageSwitcher/changeLanguageLink/link.scss new file mode 100644 index 0000000..61ef597 --- /dev/null +++ b/src/components/languageSwitcher/changeLanguageLink/link.scss @@ -0,0 +1,21 @@ +.languageLink { + color: #666; + border-bottom: 1px dotted #666; + text-decoration: none; + transition: .25s; + cursor: pointer; +} + +.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, .2); + + background-size: cover; +} diff --git a/src/components/languageSwitcher/index.js b/src/components/languageSwitcher/index.js new file mode 100644 index 0000000..d0aa598 --- /dev/null +++ b/src/components/languageSwitcher/index.js @@ -0,0 +1,3 @@ +// @flow +export { default } from './LanguageSwitcher'; +export { default as ChangeLanguageLink } from './changeLanguageLink/ChangeLanguageLink'; diff --git a/src/components/languageSwitcher/languageSwitcher.scss b/src/components/languageSwitcher/languageSwitcher.scss index e8a68da..09e9eee 100644 --- a/src/components/languageSwitcher/languageSwitcher.scss +++ b/src/components/languageSwitcher/languageSwitcher.scss @@ -76,6 +76,7 @@ $languageListBorderStyle: 1px solid $languageListBorderColor; transition: background-color .25s; cursor: pointer; overflow: hidden; + line-height: 1; &:hover { background-color: $whiteButtonLight; diff --git a/src/components/profile/Profile.js b/src/components/profile/Profile.js index 8b302e3..250c782 100644 --- a/src/components/profile/Profile.js +++ b/src/components/profile/Profile.js @@ -1,28 +1,22 @@ +// @flow import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - +import { connect } from 'react-redux'; import { FormattedMessage as Message, FormattedRelative as Relative } from 'react-intl'; import { Link } from 'react-router-dom'; import Helmet from 'react-helmet'; - -import { localeFlags } from 'components/i18n'; -import LANGS from 'i18n/index.json'; - -import { userShape } from 'components/user/User'; - +import { ChangeLanguageLink } from 'components/languageSwitcher'; import ProfileField from './ProfileField'; import styles from './profile.scss'; import profileForm from './profileForm.scss'; import messages from './Profile.intl.json'; - import RulesPage from 'pages/rules/RulesPage'; -class Profile extends Component { - static displayName = 'Profile'; - static propTypes = { - user: userShape, - createLanguageSwitcherPopup: PropTypes.func.isRequired, - }; +import type { User } from 'components/user'; + +class Profile extends Component<{ + user: User +}> { + UUID: ?HTMLDivElement; render() { const { user } = this.props; @@ -30,7 +24,7 @@ class Profile extends Component { return (
    - {(pageTitle) => ( + {(pageTitle: string) => (

    {pageTitle} @@ -90,14 +84,7 @@ class Profile extends Component { } - value={ - - - {LANGS[user.lang].name} - - } + value={} /> ({ user: state.user, -}), { - createLanguageSwitcherPopup: () => createPopup(LanguageSwitcher), -})(Profile); +}))(Profile); diff --git a/src/components/profile/profile.scss b/src/components/profile/profile.scss index f66ebfe..cfb856a 100644 --- a/src/components/profile/profile.scss +++ b/src/components/profile/profile.scss @@ -68,28 +68,6 @@ $formColumnWidth: 416px; flex-grow: 1; } -.language { - color: #666; - border-bottom: 1px dotted #666; - text-decoration: none; - transition: .25s; - cursor: pointer; -} - -.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, .2); - - background-size: cover; -} - .uuidValue { composes: paramName; composes: paramValue; diff --git a/src/components/user/reducer.js b/src/components/user/reducer.js index d95751f..361061a 100644 --- a/src/components/user/reducer.js +++ b/src/components/user/reducer.js @@ -12,7 +12,7 @@ export type User = {| isGuest: bool, isActive: bool, isOtpEnabled: bool, - passwordChangedAt: ?number, + passwordChangedAt: number, hasMojangUsernameCollision: bool, maskedEmail?: string, shouldAcceptRules?: bool, @@ -32,7 +32,7 @@ const defaults: User = { isActive: false, isOtpEnabled: false, shouldAcceptRules: false, // whether user need to review updated rules - passwordChangedAt: null, + passwordChangedAt: 0, hasMojangUsernameCollision: false, // frontend specific attributes diff --git a/src/i18n/index.json b/src/i18n/index.json index dddafde..a037f81 100644 --- a/src/i18n/index.json +++ b/src/i18n/index.json @@ -1,71 +1,83 @@ { "be": { + "code": "be", "name": "Беларуская", "englishName": "Belarusian", "progress": 100, "isReleased": true }, "en": { + "code": "en", "name": "English, UK", "englishName": "English, UK", "progress": 100, "isReleased": true }, "fr": { + "code": "fr", "name": "Français", "englishName": "French", "progress": 85.6, "isReleased": true }, "id": { + "code": "id", "name": "Bahasa Indonesia", "englishName": "Indonesian", "progress": 96.2, "isReleased": true }, "lt": { + "code": "lt", "name": "Lietuvių", "englishName": "Lithuanian", "progress": 92.8, "isReleased": false }, "pl": { + "code": "pl", "name": "Polski", "englishName": "Polish", "progress": 81.8, "isReleased": false }, "pt": { + "code": "pt", "name": "Português do Brasil", "englishName": "Portuguese, Brazilian", "progress": 96.2, "isReleased": true }, "ro": { + "code": "ro", "name": "Română", "englishName": "Romanian", "progress": 81.8, "isReleased": false }, "ru": { + "code": "ru", "name": "Русский", "englishName": "Russian", "progress": 100, "isReleased": true }, "sl": { + "code": "sl", "name": "Slovenščina", "englishName": "Slovenian", "progress": 82.2, "isReleased": false }, "uk": { + "code": "uk", "name": "Українська", "englishName": "Ukrainian", "progress": 85.6, "isReleased": true }, "vi": { + "code": "vi", "name": "Tiếng Việt", "englishName": "Vietnamese", "progress": 96.2, diff --git a/webpack.config.js b/webpack.config.js index 4dc93c7..b0d61a2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -268,7 +268,7 @@ if (isProduction) { ]; webpackConfig.entry.vendor = Object.keys(packageJson.dependencies) - .filter((module) => ignoredPlugins.indexOf(module) === -1); + .filter((module) => !ignoredPlugins.includes(module)); } else { webpackConfig.plugins.push( new webpack.DllReferencePlugin({