Migrate project's code base to TypeScript

This commit is contained in:
ErickSkrauch 2019-05-13 02:27:07 +03:00
parent c2c920d5f7
commit 71021a52c9
72 changed files with 1449 additions and 1182 deletions

View File

@ -33,6 +33,10 @@
"@babel/plugin-proposal-export-default-from": "^7.2.0", "@babel/plugin-proposal-export-default-from": "^7.2.0",
"@babel/preset-env": "^7.3.4", "@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.3.3",
"@types/react": "^16.8.17",
"@types/react-intl": "^2.3.17",
"@types/webpack-env": "^1.13.9",
"babel-eslint": "^6.0.0", "babel-eslint": "^6.0.0",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"babel-preset-react-hot": "^1.0.5", "babel-preset-react-hot": "^1.0.5",
@ -45,13 +49,14 @@
"i18n-crowdin": "file:scripts/i18n-crowdin", "i18n-crowdin": "file:scripts/i18n-crowdin",
"intl-json-loader": "file:./webpack-utils/intl-json-loader", "intl-json-loader": "file:./webpack-utils/intl-json-loader",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
"prop-types": "^15.7.2",
"text2png-loader": "file:./webpack-utils/text2png-loader", "text2png-loader": "file:./webpack-utils/text2png-loader",
"typescript": "^3.4.5",
"webpack": "^4.29.6", "webpack": "^4.29.6",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-cli": "^3.3.0", "webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.2.1" "webpack-dev-server": "^3.2.1"
}, },
"engines": { "engines": {
"node": ">=7.6.0" "node": ">=8.0"
} }
} }

View File

@ -1,15 +1,19 @@
import React from 'react'; import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE } from './params'; import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE } from 'params';
import BaseLayout from 'components/BaseLayout'; import { BaseLayout } from 'components';
export default function App({type, payload = {}}) { export interface Params {
let { locale } = payload; type: string;
payload: {
locale: string;
[key: string]: any;
};
}
const App: FunctionComponent<Params> = ({ type, payload: { locale, ...params } }) => {
if (!locale || SUPPORTED_LANGUAGES.indexOf(locale) === -1) { if (!locale || SUPPORTED_LANGUAGES.indexOf(locale) === -1) {
locale = DEFAULT_LANGUAGE; locale = DEFAULT_LANGUAGE;
} }
@ -21,17 +25,13 @@ export default function App({type, payload = {}}) {
const { default: Email } = require(`emails/${type}/index`); const { default: Email } = require(`emails/${type}/index`);
return ( return (
// @ts-ignore have no idea why
<IntlProvider locale={locale} messages={messages}> <IntlProvider locale={locale} messages={messages}>
<BaseLayout> <BaseLayout>
<Email {...payload} /> <Email {...params} />
</BaseLayout> </BaseLayout>
</IntlProvider> </IntlProvider>
); );
}
App.propTypes = {
type: PropTypes.string.isRequired,
payload: PropTypes.shape({
locale: PropTypes.string,
}),
}; };
export default App;

View File

@ -1,10 +1,10 @@
import React from 'react'; import React, { FunctionComponent } from 'react';
import styles from './styles'; import styles from './styles';
import { Table } from 'components/table'; import { Table } from 'components/table';
export default function BaseLayout(props) { const BaseLayout: FunctionComponent = ({ children }) => {
return ( return (
<Table style={styles.body}> <Table style={styles.body}>
<tr> <tr>
@ -12,7 +12,7 @@ export default function BaseLayout(props) {
&nbsp; &nbsp;
</td> </td>
<td style={styles.container}> <td style={styles.container}>
{props.children} {children}
</td> </td>
<td> <td>
&nbsp; &nbsp;
@ -20,4 +20,6 @@ export default function BaseLayout(props) {
</tr> </tr>
</Table> </Table>
); );
} };
export default BaseLayout;

View File

@ -1,6 +1,6 @@
import React from 'react'; import React, { FunctionComponent } from 'react';
export default function Html(props) { const Html: FunctionComponent = ({ children }) => {
return ( return (
<html> <html>
<head> <head>
@ -10,8 +10,10 @@ export default function Html(props) {
<body style={{ <body style={{
margin: 0 margin: 0
}}> }}>
{props.children} {children}
</body> </body>
</html> </html>
); );
} };
export default Html;

View File

@ -1,15 +1,21 @@
import React from 'react'; import React, { FunctionComponent, ReactElement } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage as Message } from 'react-intl'; import { FormattedMessage as Message } from 'react-intl';
import { colors, green } from 'components/ui/colors'; import { Colors } from 'components/ui/colors';
import { Button, Input } from 'components/ui'; import { Button, Input } from 'components/ui';
import styles from './styles'; import styles from './styles';
import messages from './messages.intl.json'; import messages from './messages.intl.json';
export default function Code({code, link, label, color = green}) { interface Props {
code: string;
link: string;
label: ReactElement;
color?: Colors;
}
const Code: FunctionComponent<Props> = ({ code, link, label, color = 'green' }) => {
return ( return (
<div style={styles.codeWrapper}> <div style={styles.codeWrapper}>
<div> <div>
@ -28,11 +34,6 @@ export default function Code({code, link, label, color = green}) {
</div> </div>
</div> </div>
); );
}
Code.propTypes = {
code: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
color: PropTypes.oneOf(Object.values(colors))
}; };
export default Code;

View File

@ -1,21 +0,0 @@
export default {
codeWrapper: {
paddingTop: '20px',
textAlign: 'center'
},
confirmEmailButton: {
paddingLeft: '50px',
paddingRight: '50px'
},
or: {
fontSize: '12px',
paddingTop: '5px'
},
codeLabel: {
paddingTop: '1px',
fontSize: '16px'
},
code: {
paddingTop: '5px'
}
};

View File

@ -0,0 +1,25 @@
import { CSSProperties } from 'react';
const styles: { [key: string]: CSSProperties } = {
codeWrapper: {
paddingTop: '20px',
textAlign: 'center',
},
confirmEmailButton: {
paddingLeft: '50px',
paddingRight: '50px',
},
or: {
fontSize: '12px',
paddingTop: '5px',
},
codeLabel: {
paddingTop: '1px',
fontSize: '16px',
},
code: {
paddingTop: '5px',
},
};
export default styles;

View File

@ -1,5 +0,0 @@
import Code from './code/Code';
export {
Code
};

View File

@ -0,0 +1 @@
export { default as Code } from './code/Code';

View File

@ -1,5 +0,0 @@
import Html from './Html';
export {
Html
};

2
src/components/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { default as HTML } from './Html';
export { default as BaseLayout } from './BaseLayout';

View File

@ -1,11 +0,0 @@
import React from 'react';
import styles from './styles';
export default function Content(props) {
return (
<div style={styles.content}>
{props.children}
</div>
);
}

View File

@ -0,0 +1,13 @@
import React, { FunctionComponent } from 'react';
import styles from './styles';
const Content: FunctionComponent = ({ children }) => {
return (
<div style={styles.content}>
{children}
</div>
);
};
export default Content;

View File

@ -1,24 +0,0 @@
import {green} from 'components/ui/colors';
export default {
footer: {
borderTop: `10px solid ${green.color}`,
background: '#DDD8CE',
height: '135px'
},
footerText: {
verticalAlign: 'middle',
paddingLeft: '30px',
fontSize: '13px',
color: '#7A7A7A'
},
footerLink: {
color: '#7A7A7A',
textDecoration: 'none',
borderBottom: '1px dashed #7A7A7A'
},
footerLogo: {
padding: '0 30px',
textAlign: 'center'
}
};

View File

@ -0,0 +1,27 @@
import { green } from 'components/ui/colors';
import { CSSProperties } from 'react';
const styles: { [key: string]: CSSProperties } = {
footer: {
borderTop: `10px solid ${green.base}`,
background: '#DDD8CE',
height: '135px',
},
footerText: {
verticalAlign: 'middle',
paddingLeft: '30px',
fontSize: '13px',
color: '#7A7A7A',
},
footerLink: {
color: '#7A7A7A',
textDecoration: 'none',
borderBottom: '1px dashed #7A7A7A',
},
footerLogo: {
padding: '0 30px',
textAlign: 'center',
},
};
export default styles;

View File

@ -1,5 +1,4 @@
import React from 'react'; import React, { FunctionComponent, ReactElement } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage as Message } from 'react-intl'; import { FormattedMessage as Message } from 'react-intl';
import { Table } from 'components/table'; import { Table } from 'components/table';
@ -7,7 +6,12 @@ import { Table } from 'components/table';
import styles from './styles'; import styles from './styles';
import messages from './messages.intl.json'; import messages from './messages.intl.json';
export default function Userbar({username, title}) { interface Props {
username: string;
title: ReactElement;
}
const Userbar: FunctionComponent<Props> = ({ username, title }) => {
return ( return (
<Table style={styles.headerImage}> <Table style={styles.headerImage}>
<tr> <tr>
@ -21,9 +25,6 @@ export default function Userbar({username, title}) {
</tr> </tr>
</Table> </Table>
); );
}
Userbar.propTypes = {
username: PropTypes.string,
title: PropTypes.node
}; };
export default Userbar;

View File

@ -1,16 +0,0 @@
import background from './background.jpg';
export default {
headerImage: {
height: '200px',
backgroundImage: `url(${background})`
},
headerTextContainer: {
color: '#fff',
textAlign: 'center',
verticalAlign: 'middle'
},
welcomeUsername: {
fontSize: '20px'
},
};

View File

@ -0,0 +1,19 @@
import background from './background.jpg';
import { CSSProperties } from 'react';
const styles: { [key: string]: CSSProperties } = {
headerImage: {
height: '200px',
backgroundImage: `url(${background})`,
},
headerTextContainer: {
color: '#fff',
textAlign: 'center',
verticalAlign: 'middle',
},
welcomeUsername: {
fontSize: '20px',
},
};
export default styles;

View File

@ -1,11 +0,0 @@
import Userbar from './userbar/Userbar';
import Header from './header/Header';
import Content from './content/Content';
import Footer from './footer/Footer';
export {
Userbar,
Header,
Content,
Footer
};

View File

@ -0,0 +1,4 @@
export { default as Userbar } from './userbar/Userbar';
export { default as Header } from './header/Header';
export { default as Content } from './content/Content';
export { default as Footer } from './footer/Footer';

View File

@ -1,26 +1,24 @@
import React from 'react'; import React, { FunctionComponent } from 'react';
import { Table } from 'components/table'; import { Table } from 'components/table';
import styles from './styles'; import styles from './styles';
import logoImage from './logo.png'; import logoImage from './logo.png';
export default function Userbar() { const Userbar: FunctionComponent = () => {
return ( return (
<Table style={styles.userbar}> <Table style={styles.userbar}>
<tr> <tr>
<td style={styles.marginColumn} /> <td style={styles.marginColumn} />
<td style={styles.logoColumn}> <td style={styles.logoColumn}>
<a href="http://ely.by" style={styles.logo}> <a href="http://ely.by" style={styles.logo}>
{/* TODO: здесь нужно динамически сформировать название, т.к. может быть Ёly.by */} <img src={logoImage} alt="Ely.by" style={styles.logoImage} />
<img src={logoImage} alt="Ely.by" style={{
width: '65px',
verticalAlign: 'middle'
}} />
</a> </a>
</td> </td>
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
</Table> </Table>
); );
} };
export default Userbar;

View File

@ -1,21 +0,0 @@
import {green} from 'components/ui/colors';
export default {
userbar: {
background: green.color,
height: '50px'
},
marginColumn: {
width: '20px'
},
logoColumn: {
width: '1%',
verticalAlign: 'middle',
background: green.dark
},
logo: {
padding: '0 13px',
display: 'block',
lineHeight: '50px'
}
};

View File

@ -0,0 +1,25 @@
import { green } from 'components/ui/colors';
export default {
userbar: {
background: green.base,
height: '50px',
},
marginColumn: {
width: '20px',
},
logoColumn: {
width: '1%',
verticalAlign: 'middle',
background: green.dark,
},
logo: {
padding: '0 13px',
display: 'block',
lineHeight: '50px',
},
logoImage: {
width: '65px',
verticalAlign: 'middle',
},
};

View File

@ -3,9 +3,9 @@ export default {
backgroundColor: '#EBE8E1', backgroundColor: '#EBE8E1',
width: '100%', width: '100%',
fontFamily: 'Roboto, Arial, sans-serif', fontFamily: 'Roboto, Arial, sans-serif',
color: '#444' color: '#444',
}, },
container: { container: {
width: '600px' width: '600px',
} },
}; };

View File

@ -1,16 +0,0 @@
import React from 'react';
import styles from './styles';
export default function Table(props) {
return (
<table cellPadding="0" cellSpacing="0" style={{
...styles.table,
...props.style
}}>
<tbody>
{props.children}
</tbody>
</table>
);
}

View File

@ -0,0 +1,22 @@
import React, { CSSProperties, FunctionComponent } from 'react';
import styles from './styles';
interface Props {
style?: CSSProperties;
}
const Table: FunctionComponent<Props> = ({ children, style }) => {
return (
<table cellPadding="0" cellSpacing="0" style={{
...styles.table,
...style
}}>
<tbody>
{children}
</tbody>
</table>
);
};
export default Table;

View File

@ -1 +0,0 @@
export Table from './Table';

View File

@ -0,0 +1 @@
export { default as Table } from './Table';

View File

@ -1,8 +0,0 @@
export default {
table: {
borderCollapse: 'collapse',
msoTableLspace: '0pt',
msoTableRspace: '0pt',
width: '100%'
}
};

View File

@ -0,0 +1,13 @@
import { CSSProperties } from 'react';
const styles: { [key: string]: CSSProperties } = {
table: {
borderCollapse: 'collapse',
// @ts-ignore
msoTableLspace: '0pt',
msoTableRspace: '0pt',
width: '100%'
},
};
export default styles;

View File

@ -1,34 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './styles';
import { colors, green } from 'components/ui/colors';
export default class Button extends Component {
static propTypes = {
label: PropTypes.node.isRequired,
color: PropTypes.oneOf(Object.values(colors))
};
static defaultProps = {
color: green
};
render() {
const {props} = this;
const {color, label} = props;
return (
<div style={{
...styles.button,
...styles[color],
...props.style
}}>
{label}
</div>
);
}
}

View File

@ -0,0 +1,26 @@
import React, { CSSProperties, FunctionComponent, ReactElement } from 'react';
import { Colors, green } from 'components/ui/colors';
import styles from './styles';
interface Props {
label: ReactElement;
style?: CSSProperties;
color?: Colors;
}
const Button: FunctionComponent<Props> = ({ label, style, color = 'green' }) => {
return (
<div style={{
...styles.button,
...styles[color],
...style,
}}>
{label}
</div>
);
};
export default Button;

View File

@ -1 +0,0 @@
export Button from './Button';

View File

@ -0,0 +1 @@
export { default } from './Button';

View File

@ -1,25 +0,0 @@
import { colors } from 'components/ui/colors';
/**
* @param {Color} color
* @return {{backgroundColor: *}}
*/
function generateColor(color) {
return {
backgroundColor: color.color
};
}
const styles = {
button: {
padding: '0 13px',
lineHeight: '50px',
display: 'inline-block'
}
};
Object.values(colors).forEach((color) => {
styles[color] = generateColor(color);
});
export default styles;

View File

@ -0,0 +1,27 @@
import { Color, Colors, colors } from 'components/ui/colors';
import { CSSProperties } from 'react';
function generateColor({ base }: Color): CSSProperties {
return {
backgroundColor: base,
};
}
type Styles = {
[key: string]: CSSProperties;
}
const styles: Styles = {
button: {
padding: '0 13px',
lineHeight: '50px',
display: 'inline-block',
},
};
Object.keys(colors).forEach((colorName) => {
// TS has error when trying to recognize keys types, so we cast it manually
styles[colorName] = generateColor(colors[colorName as any as Colors]);
});
export default styles;

View File

@ -1,48 +0,0 @@
export class Color {
constructor(name, base, light, dark) {
this._name = name;
this._base = base;
this._light = light;
this._dark = dark;
}
get name() {
return this._name;
}
get color() {
return this._base;
}
get light() {
return this._light;
}
get dark() {
return this._dark;
}
toString() {
return this.name;
}
}
export const green = new Color('green', '#207e5c', '#379070', '#1a6449');
export const blue = new Color('blue', '#5b9aa9', '#71a6b2', '#568297');
export const darkBlue = new Color('darkBlue', '#28555b', '#3e6164', '#233d49');
export const violet = new Color('violet', '#6b5b8c', '#816795', '#66437a');
export const lightViolet = new Color('lightViolet', '#8b5d79', '#a16982', '#864567');
export const orange = new Color('orange', '#dd8650', '#f39259', '#d86e3e');
export const red = new Color('red', '#e66c69', '#e15457', '#fc7872');
export const colors = {
green,
blue,
darkBlue,
violet,
lightViolet,
orange,
red
};

View File

@ -0,0 +1,63 @@
export type Colors =
| 'green'
| 'blue'
| 'darkBlue'
| 'violet'
| 'lightViolet'
| 'orange'
| 'red'
;
export interface Color {
base: string;
light: string;
dark: string;
}
export const green: Color = {
base: '#207e5c',
light: '#379070',
dark: '#1a6449',
};
export const blue: Color = {
base: '#5b9aa9',
light: '#71a6b2',
dark: '#568297',
};
export const darkBlue: Color = {
base: '#28555b',
light: '#3e6164',
dark: '#233d49',
};
export const violet: Color = {
base: '#6b5b8c',
light: '#816795',
dark: '#66437a',
};
export const lightViolet: Color = {
base: '#8b5d79',
light: '#a16982',
dark: '#864567',
};
export const orange: Color = {
base: '#dd8650',
light: '#f39259',
dark: '#d86e3e',
};
export const red: Color = {
base: '#e66c69',
light: '#e15457',
dark: '#fc7872',
};
type ColorsMap = { [key in Colors]: Color };
export const colors: ColorsMap = {
green,
blue,
darkBlue,
violet,
lightViolet,
orange,
red,
};

View File

@ -1,32 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { colors, green } from 'components/ui/colors';
import styles from './styles';
export default class Input extends Component {
static propTypes = {
value: PropTypes.string.isRequired,
color: PropTypes.oneOf(Object.values(colors)),
};
static defaultProps = {
color: green,
};
render() {
const {props} = this;
const {value, color, style} = props;
return (
<div style={{
...styles.input,
...styles[color],
...style
}}>
{value}
</div>
);
}
}

View File

@ -0,0 +1,25 @@
import React, { CSSProperties, FunctionComponent } from 'react';
import { Colors } from 'components/ui/colors';
import styles from './styles';
interface Props {
value: string;
color?: Colors;
style?: CSSProperties;
}
const Input: FunctionComponent<Props> = ({ value, style, color = 'green' }) => {
return (
<div style={{
...styles.input,
...styles[color],
...style
}}>
{value}
</div>
);
};
export default Input;

View File

@ -1 +0,0 @@
export Input from './Input';

View File

@ -0,0 +1 @@
export { default } from './Input';

View File

@ -1,30 +0,0 @@
import { colors } from 'components/ui/colors';
/**
* @param {Color} color
* @return {{backgroundColor: *}}
*/
function generateColor(color) {
return {
borderColor: color.color,
color: color.dark
};
}
const styles = {
input: {
backgroundColor: '#fff',
padding: '0 30px',
lineHeight: '50px',
fontSize: '18px',
display: 'inline-block',
border: '3px solid transparent',
color: '#444'
}
};
Object.values(colors).forEach((color) => {
styles[color] = generateColor(color);
});
export default styles;

View File

@ -0,0 +1,28 @@
import { CSSProperties } from 'react';
import { Color, Colors, colors } from 'components/ui/colors';
const styles: { [key: string]: CSSProperties } = {
input: {
backgroundColor: '#fff',
padding: '0 30px',
lineHeight: '50px',
fontSize: '18px',
display: 'inline-block',
border: '3px solid transparent',
color: '#444',
},
};
function generateColor({ base, dark }: Color): CSSProperties {
return {
borderColor: base,
color: dark,
};
}
Object.keys(colors).forEach((color) => {
// TS has error when trying to recognize keys types, so we cast it manually
styles[color] = generateColor(colors[color as any as Colors]);
});
export default styles;

View File

@ -1,94 +0,0 @@
import React, { Component } from 'react';
import App from 'App';
import List from './List';
import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES } from 'params';
const AVAILABLE_EMAILS = require.context('emails', true, /index\.js$/).keys().map((path) => path.split('/')[1]);
export default class DevApp extends Component {
state = {
locale: DEFAULT_LANGUAGE,
type: AVAILABLE_EMAILS[0],
fixture: 'default',
isMinimized: false,
};
componentWillMount() {
try {
const lastState = JSON.parse(localStorage.getItem('emailRendererState'));
lastState && this.setState(lastState);
} catch (err) {/* no state was saved */}
}
componentWillUpdate(nextProps, nextState) {
localStorage.setItem('emailRendererState', JSON.stringify(nextState));
}
render() {
const {locale, type, isMinimized} = this.state;
let {fixture} = this.state;
let fixturesAvailable = {};
try {
fixturesAvailable = require(`emails/${type}/fixtures`).default;
} catch (err) {/* no fixtures available */}
if (!fixturesAvailable[fixture]) {
fixture = 'default';
}
const payload = {
locale,
...(fixturesAvailable[fixture] || {})
};
return (
<div>
<div style={isMinimized ? {
opacity: 0.4,
position: 'fixed'
} : {}}>
[<a href="#" style={{textDecoration: 'none', padding: '6px'}} onClick={this.onMinimizeToggle}>
{isMinimized ? '+' : '-'}
</a>]
<div style={isMinimized ? {display: 'none'} : {}}>
<List label="Lang"
items={SUPPORTED_LANGUAGES}
active={locale}
onChange={this.onLocaleChange}
/>
<List label="Email"
items={AVAILABLE_EMAILS}
active={type}
onChange={this.onTypeChange}
/>
<List label="Fixtures"
items={Object.keys(fixturesAvailable)}
active={fixture}
onChange={this.onFixtureChange}
/>
</div>
</div>
<App type={type} payload={payload} />
</div>
);
}
onLocaleChange = (locale) => this.setState({locale});
onTypeChange = (type) => this.setState({type});
onFixtureChange = (fixture) => this.setState({fixture});
onMinimizeToggle = (event) => {
event.preventDefault();
this.setState({
isMinimized: !this.state.isMinimized
});
}
}

109
src/devTools/DevApp.tsx Normal file
View File

@ -0,0 +1,109 @@
import React, { BaseSyntheticEvent, FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES } from 'params';
import App from 'App';
import List from './List';
const AVAILABLE_TEMPLATES = require.context('emails', true, /index\.[jt]s$/).keys().map((path) => path.split('/')[1]);
interface LocalStorageState {
locale: string;
template: string;
fixture: string;
isMinimized: boolean;
}
const DevApp: FunctionComponent = () => {
const [ locale, setLocale ] = useState(DEFAULT_LANGUAGE);
const [ template, setTemplate ] = useState(AVAILABLE_TEMPLATES[0]);
const [ fixture, setFixture ] = useState('default');
const [ isMinimized, setIsMinimized ] = useState(false);
// Load stored state from local storage on the first run
useEffect(() => {
let state: LocalStorageState;
try {
state = JSON.parse(localStorage.getItem('emailRendererState') || '');
if (!state) {
return;
}
} catch (err) {
return;
}
setLocale(state.locale);
setTemplate(state.template);
setFixture(state.fixture);
setIsMinimized(state.isMinimized);
}, []);
// Store current state to the local storage when any param is changed
useEffect(() => {
const state: LocalStorageState = { locale, template, fixture, isMinimized };
localStorage.setItem('emailRendererState', JSON.stringify(state));
}, [locale, template, fixture, isMinimized]);
const availableFixtures = useMemo(() => {
try {
return require(`emails/${template}/fixtures`).default;
} catch (err) {
return {};
}
}, [template]);
const payload = useMemo(() => ({
locale,
...(availableFixtures[fixture] || availableFixtures['default'] || {}),
}), [locale, availableFixtures, fixture]);
const onMinimizeClick = useCallback((event: BaseSyntheticEvent) => {
event.preventDefault();
setIsMinimized(!isMinimized);
}, [isMinimized, setIsMinimized]);
return (
<div>
<div style={isMinimized ? {
opacity: 0.4,
position: 'fixed',
} : {}}>
[<a
href="#"
style={{
textDecoration: 'none',
padding: '6px',
}}
onClick={onMinimizeClick}
>
{isMinimized ? '+' : '-'}
</a>]
<div style={isMinimized ? {display: 'none'} : {}}>
<List label="Lang"
items={SUPPORTED_LANGUAGES}
active={locale}
onChange={setLocale}
/>
<List label="Email"
items={AVAILABLE_TEMPLATES}
active={template}
onChange={setTemplate}
/>
<List label="Fixtures"
items={Object.keys(availableFixtures)}
active={fixture}
onChange={setFixture}
/>
</div>
</div>
<App type={template} payload={payload} />
</div>
);
};
export default DevApp;

View File

@ -1,31 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
export default function List({label, items, active, onChange}) {
return (
<div>
{label}:
{items.map((key) =>
<a href="#"
key={key}
onClick={(event) => {
event.preventDefault();
onChange && onChange(key);
}}
style={{
padding: '0 5px',
color: active === key ? 'red' : ''
}}
>{key}</a>
)}
</div>
);
}
List.propTypes = {
label: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.string).isRequired,
active: PropTypes.string,
onChange: PropTypes.func
};

34
src/devTools/List.tsx Normal file
View File

@ -0,0 +1,34 @@
import React, { FunctionComponent } from 'react';
interface Props {
label: string;
items: ReadonlyArray<string>;
active: string;
onChange: (item: string) => any;
}
const List: FunctionComponent<Props> = ({ label, items, active, onChange = () => {} }) => {
return (
<div>
{label}:
{items.map((item) =>
<a
href="#"
key={item}
onClick={(event) => {
event.preventDefault();
onChange(item);
}}
style={{
padding: '0 5px',
color: active === item ? 'red' : '',
}}
>
{item}
</a>
)}
</div>
);
};
export default List;

View File

@ -1,3 +0,0 @@
import DevApp from './DevApp';
export default DevApp;

1
src/devTools/index.ts Normal file
View File

@ -0,0 +1 @@
export { default } from './DevApp';

View File

@ -1,16 +1,20 @@
import React from 'react'; import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage as Message, FormattedHTMLMessage as HTMLMessage } from 'react-intl'; import { FormattedMessage as Message, FormattedHTMLMessage as HTMLMessage } from 'react-intl';
import { Userbar, Header, Content, Footer } from 'components/layout'; import { Userbar, Header, Content, Footer } from 'components/layout';
import { Table } from 'components/table'; import { Table } from 'components/table';
import { Code } from 'components/blocks'; import { Code } from 'components/blocks';
import { lightViolet } from 'components/ui/colors';
import styles from './styles'; import styles from './styles';
import messages from './messages.intl.json'; import messages from './messages.intl.json';
export default function ForgotPassword({username, link, code}) { interface Props {
username: string;
link: string;
code: string;
}
const ForgotPassword: FunctionComponent<Props> = ({ username, link, code }) => {
return ( return (
<div> <div>
<Userbar /> <Userbar />
@ -30,7 +34,7 @@ export default function ForgotPassword({username, link, code}) {
</tr> </tr>
<tr> <tr>
<td> <td>
<Code code={code} link={link} color={lightViolet} label={ <Code code={code} link={link} color="lightViolet" label={
<HTMLMessage {...messages.continue_image} /> <HTMLMessage {...messages.continue_image} />
} /> } />
</td> </td>
@ -41,10 +45,6 @@ export default function ForgotPassword({username, link, code}) {
<Footer /> <Footer />
</div> </div>
); );
}
ForgotPassword.propTypes = {
username: PropTypes.string,
link: PropTypes.string,
code: PropTypes.string
}; };
export default ForgotPassword;

View File

@ -2,6 +2,6 @@ export default {
default: { default: {
username: 'ErickSkrauch', username: 'ErickSkrauch',
code: 'I7SP06BUTLLM8MA03O', code: 'I7SP06BUTLLM8MA03O',
link: 'https://account.ely.by/activation/I7SP06BUTLLM8MA03O' link: 'https://account.ely.by/activation/I7SP06BUTLLM8MA03O',
}, },
}; };

View File

@ -1,3 +0,0 @@
import ForgotPassword from './ForgotPassword';
export default ForgotPassword;

View File

@ -0,0 +1 @@
export { default } from './ForgotPassword';

View File

@ -1,10 +1,9 @@
export default { export default {
contentCenterCell: { contentCenterCell: {
textAlign: 'center' textAlign: 'center',
}, },
paragraph: { paragraph: {
fontSize: '16px', fontSize: '16px',
lineHeight: '125%' lineHeight: '125%',
}, },
}; };

View File

@ -1,10 +1,8 @@
import React from 'react'; import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage as Message, FormattedHTMLMessage as HTMLMessage } from 'react-intl'; import { FormattedMessage as Message, FormattedHTMLMessage as HTMLMessage } from 'react-intl';
import { Userbar, Header, Content, Footer } from 'components/layout'; import { Userbar, Header, Content, Footer } from 'components/layout';
import { Table } from 'components/table'; import { Table } from 'components/table';
import { blue } from 'components/ui/colors';
import { Code } from 'components/blocks'; import { Code } from 'components/blocks';
import styles from './styles'; import styles from './styles';
@ -14,7 +12,13 @@ import violetManImage from './images/violetMan.png';
import orangeManImage from './images/orangeMan.png'; import orangeManImage from './images/orangeMan.png';
import darkBlueManImage from './images/darkBlueMan.png'; import darkBlueManImage from './images/darkBlueMan.png';
export default function Register({username, link, code}) { interface Props {
username: string;
link: string;
code: string;
}
const Register: FunctionComponent<Props> = ({ username, link, code }) => {
return ( return (
<div> <div>
<Userbar /> <Userbar />
@ -34,7 +38,7 @@ export default function Register({username, link, code}) {
</tr> </tr>
<tr> <tr>
<td> <td>
<Code code={code} link={link} color={blue} label={ <Code code={code} link={link} color="blue" label={
<HTMLMessage {...messages.confirm_email_image} /> <HTMLMessage {...messages.confirm_email_image} />
} /> } />
</td> </td>
@ -42,7 +46,7 @@ export default function Register({username, link, code}) {
<tr> <tr>
<td style={{ <td style={{
...styles.contentCenterCell, ...styles.contentCenterCell,
...styles.whatsNextText ...styles.whatsNextText,
}}> }}>
<HTMLMessage {...messages.whats_next_image} /> <HTMLMessage {...messages.whats_next_image} />
</td> </td>
@ -52,10 +56,7 @@ export default function Register({username, link, code}) {
<Table> <Table>
<tr> <tr>
<td style={styles.todoItemIcon}> <td style={styles.todoItemIcon}>
<img src={violetManImage} style={{ <img src={violetManImage} style={styles.todoItemIconImage} />
width: '25px',
verticalAlign: 'middle'
}} />
</td> </td>
<td style={styles.todoItemContent}> <td style={styles.todoItemContent}>
<HTMLMessage {...messages.choose_you_skin_image} /> <HTMLMessage {...messages.choose_you_skin_image} />
@ -75,10 +76,7 @@ export default function Register({username, link, code}) {
<Table> <Table>
<tr> <tr>
<td style={styles.todoItemIcon}> <td style={styles.todoItemIcon}>
<img src={orangeManImage} style={{ <img src={orangeManImage} style={styles.todoItemIconImage} />
width: '25px',
verticalAlign: 'middle'
}} />
</td> </td>
<td style={styles.todoItemContent}> <td style={styles.todoItemContent}>
<HTMLMessage {...messages.install_our_patch_image} /> <HTMLMessage {...messages.install_our_patch_image} />
@ -98,10 +96,7 @@ export default function Register({username, link, code}) {
<Table> <Table>
<tr> <tr>
<td style={styles.todoItemIcon}> <td style={styles.todoItemIcon}>
<img src={darkBlueManImage} style={{ <img src={darkBlueManImage} style={styles.todoItemIconImage} />
width: '25px',
verticalAlign: 'middle'
}} />
</td> </td>
<td style={styles.todoItemContent}> <td style={styles.todoItemContent}>
<HTMLMessage {...messages.useTLLauncher} /> <HTMLMessage {...messages.useTLLauncher} />
@ -122,10 +117,6 @@ export default function Register({username, link, code}) {
<Footer /> <Footer />
</div> </div>
); );
}
Register.propTypes = {
username: PropTypes.string,
link: PropTypes.string,
code: PropTypes.string,
}; };
export default Register;

View File

@ -2,12 +2,11 @@ export default {
default: { default: {
username: 'ErickSkrauch', username: 'ErickSkrauch',
code: 'I7SP06BUTLLM8MA03O', code: 'I7SP06BUTLLM8MA03O',
link: 'https://account.ely.by/activation/I7SP06BUTLLM8MA03O' link: 'https://account.ely.by/activation/I7SP06BUTLLM8MA03O',
}, },
SleepWalker: { SleepWalker: {
username: 'SleepWalker', username: 'SleepWalker',
code: 'TLLM8MA03OI7SP06BU', code: 'TLLM8MA03OI7SP06BU',
link: 'https://account.ely.by/activation/TLLM8MA03OI7SP06BU' link: 'https://account.ely.by/activation/TLLM8MA03OI7SP06BU',
} },
}; };

View File

@ -1,3 +0,0 @@
import Register from './Register';
export default Register;

View File

@ -0,0 +1 @@
export { default } from './Register';

View File

@ -1,28 +0,0 @@
export default {
contentCenterCell: {
textAlign: 'center'
},
paragraph: {
fontSize: '16px',
lineHeight: '125%'
},
whatsNextText: {
paddingTop: '30px'
},
todoItem: {
paddingTop: '30px'
},
todoItemIcon: {
width: '46px',
verticalAlign: 'top'
},
todoItemContent: {
verticalAlign: 'top'
},
todoItemText: {
paddingTop: '3px'
}
};

View File

@ -0,0 +1,33 @@
import { CSSProperties } from 'react';
const styles: { [key: string]: CSSProperties } = {
contentCenterCell: {
textAlign: 'center',
},
paragraph: {
fontSize: '16px',
lineHeight: '125%',
},
whatsNextText: {
paddingTop: '30px',
},
todoItem: {
paddingTop: '30px',
},
todoItemIcon: {
width: '46px',
verticalAlign: 'top',
},
todoItemIconImage: {
width: '25px',
verticalAlign: 'middle',
},
todoItemContent: {
verticalAlign: 'top',
},
todoItemText: {
paddingTop: '3px',
},
};
export default styles;

12
src/i18n/module.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
declare module 'i18n/index.json' {
const content: {
[key: string]: {
code: string;
name: string;
englishName: string;
progress: number;
isReleased: boolean;
},
};
export default content;
}

View File

@ -12,7 +12,7 @@ if (process.env.NODE_ENV !== 'production') {
ReactDOM.render( ReactDOM.render(
<DevApp />, <DevApp />,
document.getElementById('app') document.getElementById('app'),
); );
} else { } else {
const ReactDOMServer = require('react-dom/server'); const ReactDOMServer = require('react-dom/server');
@ -20,7 +20,14 @@ if (process.env.NODE_ENV !== 'production') {
const App = require('App').default; const App = require('App').default;
module.exports = { module.exports = {
default(props) { default(props: {
type: string;
payload: {
locale: string;
[key: string]: string;
};
assetsHost?: string;
}) {
if (props.assetsHost) { if (props.assetsHost) {
// noinspection JSUnresolvedVariable // noinspection JSUnresolvedVariable
__webpack_public_path__ = props.assetsHost.replace(/\/*$/, '/'); // eslint-disable-line __webpack_public_path__ = props.assetsHost.replace(/\/*$/, '/'); // eslint-disable-line

9
src/typings/file-loader.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module "*.png" {
const content: string;
export default content;
}
declare module "*.jpg" {
const content: string;
export default content;
}

9
src/typings/json-loader.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module '*.intl.json' {
const content: {
[key: string]: {
id: string;
defaultMessage: string;
},
};
export default content;
}

24
tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "esnext",
"lib": [
"es7",
"dom",
"dom.iterable"
],
"jsx": "preserve",
"declaration": false,
"skipLibCheck": false,
"noEmit": true,
"strict": true,
"module": "ESNext",
"moduleResolution": "node",
"baseUrl": "src",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src"
]
}

View File

@ -4,6 +4,7 @@ const path = require('path');
const { ContextReplacementPlugin } = require('webpack'); const { ContextReplacementPlugin } = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const SUPPORTED_LANGUAGES = Object.keys(require('./src/i18n/index.json')); const SUPPORTED_LANGUAGES = Object.keys(require('./src/i18n/index.json'));
@ -31,7 +32,7 @@ module.exports = (env, { mode = 'development' }) => {
path.join(__dirname, 'src'), path.join(__dirname, 'src'),
path.join(__dirname, 'node_modules'), path.join(__dirname, 'node_modules'),
], ],
extensions: ['.js', '.jsx'], extensions: ['.tsx', '.ts', '.js'],
}, },
resolveLoader: { resolveLoader: {
@ -49,33 +50,32 @@ module.exports = (env, { mode = 'development' }) => {
}, },
plugins: [ plugins: [
new ContextReplacementPlugin( new ContextReplacementPlugin(/i18n/, new RegExp(`/(${SUPPORTED_LANGUAGES.join('|')})\\.json`)),
/i18n/, new RegExp(`/(${SUPPORTED_LANGUAGES.join('|')})\\.json`) new ContextReplacementPlugin(/locale-data/, new RegExp(`/(${SUPPORTED_LANGUAGES.join('|')})\\.js`)),
),
new ContextReplacementPlugin(
/locale-data/, new RegExp(`/(${SUPPORTED_LANGUAGES.join('|')})\\.js`)
),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: 'src/index.ejs', template: 'src/index.ejs',
favicon: 'src/favicon.ico', favicon: 'src/favicon.ico',
filename: 'index.html', filename: 'index.html',
inject: false, inject: false,
}), }),
new BundleAnalyzerPlugin({
openAnalyzer: false,
generateStatsFile: true,
analyzerMode: isProduction ? 'static': 'server',
}),
], ],
module: { module: {
rules: [ rules: [
{ {
test: /\.jsx?$/, test: /\.[jt]sx?$/,
exclude: /node_modules/, exclude: /node_modules/,
use: [ use: [
{ {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
presets: [ presets: [
[ ['@babel/preset-env', {
'@babel/preset-env',
{
targets: isProduction ? { targets: isProduction ? {
node: '8', node: '8',
} : { } : {
@ -84,14 +84,13 @@ module.exports = (env, { mode = 'development' }) => {
'last 1 firefox version', 'last 1 firefox version',
], ],
}, },
}, }],
], ['@babel/preset-react', {
[
'@babel/preset-react',
{
development: !isProduction, development: !isProduction,
}, }],
], ['@babel/preset-typescript', {
jsx: true,
}],
], ],
plugins: [ plugins: [
'@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-class-properties',

1241
yarn.lock

File diff suppressed because it is too large Load Diff