diff --git a/.babelrc b/.babelrc
index 90ca27d..b7a8e9d 100644
--- a/.babelrc
+++ b/.babelrc
@@ -21,6 +21,13 @@
"plugins": [
"react-hot-loader/babel",
"@babel/plugin-syntax-dynamic-import",
+ [
+ "import",
+ {
+ "libraryName": "antd-mobile",
+ "style": true
+ }
+ ],
[
"@babel/plugin-proposal-decorators",
{
diff --git a/.eslintignore b/.eslintignore
index 363b7dc..9880d5a 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,3 +2,6 @@ node_modules/
build
dist
config
+server
+app/mobx
+app/tools
diff --git a/.eslintrc b/.eslintrc
index 2307d32..cd3f937 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -36,10 +36,7 @@
],
"comma-dangle": [
"error",
- "always-multiline",
- {
- "functions": "ignore"
- }
+ "always-multiline"
],
"quotes": [
"error",
@@ -51,6 +48,13 @@
"args": "after-used"
}
],
+ "no-use-before-define": [
+ "error",
+ {
+ "functions": false,
+ "classes": false
+ }
+ ],
"no-console": "off",
"consistent-return": "off",
"react/jsx-filename-extension": [
diff --git a/.gitignore b/.gitignore
index b89ab98..f9d0647 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,9 @@
.idea
.project
+# vscode
+.vscode
+
# npm
node_modules
npm-debug.log
@@ -17,5 +20,8 @@ package-lock.json
build
dist
+# bak
+*.bak
+
# cache
.cache
diff --git a/README.md b/README.md
index 181b78a..bcfe2f4 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,152 @@
-# init-react-project
-To initialize your react project
+# react-lighter
-## 初始化 react 项目
+to initialize your react project as simple as lighting a fire
+
+初始化 react 项目
+
+## 技术栈
+
+react + mobx + router + antd + axios
+
+## 目录结构
+
+```markdown
+├── src // 项目主目录
+│ ├── assets
+│ ├── components // 可重用组件
+│ ├── index.tsx
+│ ├── interface.ts
+│ ├── mobx // mobx 注入工具
+│ ├── pages // 项目界面
+│ │ ├── 404
+│ │ └── Example
+│ ├── routes
+│ ├── tools // 脚手架
+│ ├── utils
+│ └── websocket
+└── config // webpack 配置
+```
+
+## 运行
```bash
-yarn or npm i // 下载包
-yarn dll // 预编译
-yarn start // 本地开发
-yarn build // 项目打包
-yarn server // 加载打包后的项目
+yarn or npm i
+
+yarn dll
+
+yarn start // for dev
+
+yarn build && yarn server // for prod
```
## 特点
-待更新
+### 一、热加载
+
+采用 [react-hot-loader](https://github.com/gaearon/react-hot-loader) 配合 babel,可实现样式替换、节点改变不影响 state 等功能,达到局部热加载的效果
+
+### 二、抽离 dll
+
+使用 webpack DllReferencePlugin 插件,先把 react 抽离成 dll,在后续开发中能更快加载
+
+### 三、多线程打包
+
+使用 [happypack](https://github.com/amireh/happypack) 启动多线程,实现光速打包
+
+### 四、css 处理
+
+1. 使用 [postcss](https://github.com/postcss/postcss) 提供样式兼容
+
+2. 使用 [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) 抽离 css
+
+3. 使用 [purgecss-webpack-plugin](https://github.com/FullHuman/purgecss-webpack-plugin) 去除无用 css
+
+4. 使用 [postcss-px-to-viewport](https://github.com/evrone/postcss-px-to-viewport) 规范长度单位
+
+### 五、mobx 注入
+
+参考:
+
+- [用mobx构建大型项目的最佳实践](https://juejin.im/post/5c627df76fb9a049c232e990)
+
+- [mobx 项目最佳实践](https://github.com/luruozhou/mobx-example)
+
+1. mobx 分层:views、actions、stores
+
+> views 派发 actions,由 actions 做逻辑处理后调用 stores 做数据持久化,views 通过注入的 stores 获取到最新的数据。典型的 MVC 架构
+>
+> 解决了 mobx 中数据随处可定义、逻辑交互方法随处可声明等问题
+
+2. 唯一数据源
-## TODO
+> 每个 view 绑定 store 数据源,顶层 view 保存着全局数据源,类似于 redux 的 createStore 机制
+>
+> 实现了 stores 间交叉引用的可能,同时能够达到 redux 中 initialState 的“数据恢复”机制
-1. react-hot-loader 监听 css 变化(react-hot-loader 不能监听抽离的 css 代码) √
+3. 按需实例化
+
+> 通过 [ts-plugin-mmlpx 插件](https://github.com/mmlpxjs/ts-plugin-mmlpx),实现 stores 和 actions 的自动查找和绑定、通过 Object.defineProperty 实现按需实例化,提高性能
+
+4. tools 脚手架使用
+
+> 参见 [mobx example 开发章节](https://github.com/luruozhou/mobx-example#%E5%BC%80%E5%8F%91)
+
+### 六、Axios 封装
+
+参考
+
+- [axios restful 封装](https://github.com/zhaotoday/rest)
+
+将 axios 进行 restful 风格的封装,配合 interceptor 和 histroy 进行权限验证和跳转
+
+使用:
+
+1. GET /users/:id
+
+```js
+request.setPath('users').get({ uri: 'lawler' })
+```
+
+2. POST /users
+
+```js
+request.setPath('users').post({
+ data: { email: 'lawler61@163.com', password: '123456' }
+})
+```
+
+3. PUT /users/:id
+
+```js
+request.setPath('users').put({
+ uri: 'lawler', data: { name: 'jeffery' }
+})
+```
+
+4. DELETE /users/:id
+
+```js
+request.setPath('users').delete({ uri: 'lawler' })
+```
+
+5. GET /users/:id/articles?page=2&limit=10
+
+```js
+request.setPath('users/{id}/articles').replace('lawler').get({
+ query: { page: 2, limit: 10 }
+})
+```
+
+6. PATCH /users/:id/articles/:id
+
+```js
+request.setPath('users/{id}/articles/{id}').replace('lawler', 'react 学习之路').patch({
+ data: { title: '前端学习' }
+})
+```
-2. 懒加载组件 √
+## 项目展示
-3. 去除无用 css 代码 (不能使用 css 分离,否则无法去除)√
+1. [问答系统 前端 -> https://github.com/lawler61/qa-app](https://github.com/lawler61/qa-app)
-4. 制作成脚手架
+2. [线上地址,去看看 -> https://qa.omyleon.com](https://qa.omyleon.com)
diff --git a/config/createMobxTransformer.js b/config/createMobxTransformer.js
new file mode 100644
index 0000000..77274c7
--- /dev/null
+++ b/config/createMobxTransformer.js
@@ -0,0 +1,116 @@
+// https://github.com/mmlpxjs/ts-plugin-mmlpx
+
+const ts = require('typescript')
+
+const defaultOptions = {
+ bindings: ['mStore', 'mAction']
+}
+
+const createTransformer = (options = defaultOptions) => {
+ const transformer = context => {
+ const bindings = []
+
+ let fileName, pageName
+
+ const visitor = node => {
+ if (ts.isSourceFile(node)) {
+ ;[pageName, fileName] = getFileNameFrom(node.fileName)
+ return ts.visitEachChild(node, visitor, context)
+ }
+
+ if (ts.isImportDeclaration(node)) {
+ node.forEachChild(importChild => {
+ if (
+ ts.isImportClause(importChild) &&
+ importChild.namedBindings &&
+ ts.isNamedImports(importChild.namedBindings)
+ ) {
+ importChild.namedBindings.elements.forEach(
+ ({ propertyName, name }) => {
+ // import {mStore as otherName} from './store.js'
+ const lib = node.moduleSpecifier.text // './store.js' 暂时没用到
+ const namedBinding =
+ (propertyName && propertyName.getText()) || name.getText() // mStore
+ const aliasBinding = propertyName && name.getText() //otherName
+ if (options.bindings.indexOf(namedBinding) > -1) {
+ bindings.push(aliasBinding || namedBinding)
+ }
+ }
+ )
+ }
+ })
+
+ return node
+ }
+
+ if (node.decorators) {
+ node.decorators.forEach(decorator => {
+ const { expression } = decorator
+ if (
+ ts.isIdentifier(expression) &&
+ bindings.indexOf(expression.getText()) > -1
+ ) {
+ // 调用形式 @mStore @mAction
+ decorator.expression = ts.createCall(expression, undefined, [
+ ts.createObjectLiteral([
+ ts.createPropertyAssignment(
+ ts.createLiteral('page'),
+ ts.createLiteral(`${pageName}`)
+ ),
+ ts.createPropertyAssignment(
+ ts.createLiteral('name'),
+ ts.createLiteral(`${fileName}`)
+ )
+ ])
+ ])
+ } else if (
+ ts.isCallExpression(expression) &&
+ ts.isIdentifier(expression.expression) &&
+ bindings.indexOf(expression.expression.getText()) > -1
+ ) {
+ // 调用形式 @mStore({...someProps}) @mAction({...someProps})
+ let arg0 = expression.arguments[0]
+ if (ts.isObjectLiteralExpression(arg0)) {
+ decorator.expression = ts.createCall(
+ expression.expression,
+ undefined,
+ [
+ ts.createObjectLiteral([
+ ts.createPropertyAssignment(
+ ts.createLiteral('page'),
+ ts.createLiteral(`${pageName}`)
+ ),
+ ts.createPropertyAssignment(
+ ts.createLiteral('name'),
+ ts.createLiteral(`${fileName}`)
+ ),
+ ...arg0.properties
+ ]),
+ ...expression.arguments.slice(1)
+ ]
+ )
+ }
+ }
+ })
+
+ return node
+ }
+
+ return ts.visitEachChild(node, visitor, context)
+ }
+
+ return node => ts.visitNode(node, visitor)
+ }
+
+ return transformer
+}
+
+function getFileNameFrom(path) {
+ let reg = /([^\/]+)\/(?:actions|stores)\/(.+)\.(?:t|j)sx?$/
+ let matched = path.match(reg)
+ let pageName = matched && matched[1]
+ let fileName = matched && matched[2]
+ return [pageName, fileName]
+}
+
+module.exports = createTransformer
diff --git a/config/options.js b/config/options.js
index 5442bf4..719859c 100644
--- a/config/options.js
+++ b/config/options.js
@@ -1,7 +1,6 @@
-const { main, name, author } = require('../package.json')
-
-const entryDir = main.split('/')[0]
+const { basePath, main, name, author } = require('../package.json')
+const entryDir = basePath
const outputDir = 'dist'
// 必要参数
@@ -14,9 +13,9 @@ const baseOptions = {
purifycssFile: [`${entryDir}/*.html`, `${entryDir}/**/*.js`],
assetsPath: 'assets',
moduleToDll: {
- react: ['react', 'react-dom', 'react-router-dom']
+ react: ['react', 'react-dom', 'react-router-dom'],
},
- dllFiles: ['react.dll.js', 'react.manifest.json']
+ dllFiles: ['react.dll.js', 'react.manifest.json'],
}
// 可选参数
@@ -25,11 +24,16 @@ const extraOptions = {
// 选择 true 在开发模式中 react-hot-loader 不能热加载抽离出去的 css [https://github.com/gaearon/react-hot-loader]
// 选择 false purifycss-webpack 不能去除无用的 css [https://github.com/FullHuman/purgecss-webpack-plugin]
useCssExtract: false,
- copyConfig: { // 是否有不需要处理,直接拷贝的文件
- needsCopy: true,
+ copyConfig: {
+ // 是否有不需要处理,直接拷贝的文件
+ needsCopy: false,
fromPath: `${entryDir}/docs`,
- toPath: `${outputDir}/docs`
- }
+ toPath: `${outputDir}/docs`,
+ },
}
-module.exports = Object.assign(baseOptions, { entryDir, outputDir }, extraOptions)
+module.exports = Object.assign(
+ baseOptions,
+ { entryDir, outputDir },
+ extraOptions
+)
diff --git a/config/utils.js b/config/utils.js
index 7368f2b..be70ca6 100644
--- a/config/utils.js
+++ b/config/utils.js
@@ -7,5 +7,5 @@ function resolve(...filePath) {
}
module.exports = {
- resolve
+ resolve,
}
diff --git a/config/webpack.base.config.js b/config/webpack.base.config.js
index 17291f1..3fd0ba8 100644
--- a/config/webpack.base.config.js
+++ b/config/webpack.base.config.js
@@ -9,6 +9,8 @@ const glob = require('glob-all') // require('glob')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
const HappyPack = require('happypack')
const os = require('os')
+const tsImportPluginFactory = require('ts-import-plugin')
+const createMobxTransformer = require('./createMobxTransformer')
const { resolve } = require('./utils')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }) // 根据系统的内核数量指定线程池个数
@@ -25,7 +27,7 @@ module.exports = ({
useCssExtract,
assetsPath,
copyConfig,
- dllFiles
+ dllFiles,
}) => {
const env = process.env.NODE_ENV
const isDevMode = env === 'development'
@@ -33,7 +35,7 @@ module.exports = ({
const assetOptions = {
limit: 10000,
name: `${assetsPath}/[name].[ext]`,
- publicPath: '../'
+ publicPath: '../',
}
const plugins = [
@@ -44,25 +46,25 @@ module.exports = ({
minify: isDevMode
? null
: {
- removeAttributeQuotes: true,
- collapseWhitespace: true
- }
- // favicon: './favicon.ico',
+ removeAttributeQuotes: true,
+ collapseWhitespace: true,
+ },
+ favicon: resolve(entryDir, 'assets/images/favicon.jpg'),
}),
new webpack.BannerPlugin(`created by ${author}`),
new webpack.DllReferencePlugin({
- manifest: dllWebpack
+ manifest: dllWebpack,
}),
new AddAssetHtmlPlugin({
filepath: resolve(`${outputDir}/*.dll.js`),
- includeSourcemap: false
+ includeSourcemap: false,
}),
new MiniCssExtractPlugin({
- filename: `${cssPath}/[name].[hash:8].css`
+ filename: `${cssPath}/[name].[hash:8].css`,
// chunkFilename: "[id].css"
}),
new PurgecssPlugin({
- paths: glob.sync(purifycssFile.map(url => resolve(url)), { nodir: true })
+ paths: glob.sync(purifycssFile.map(url => resolve(url)), { nodir: true }),
}),
// new PurifycssWebpack({
// paths: glob.sync(purifycssFile.map(url => resolve(url))),
@@ -72,18 +74,18 @@ module.exports = ({
id: 'babel', // loader 中指定的 id
loaders: ['babel-loader?cacheDirectory'], // 实际匹配处理的 loader
threadPool: happyThreadPool,
- verbose: true
+ verbose: true,
}),
new webpack.SourceMapDevToolPlugin({
- filename: '[file].map'
- })
+ filename: '[file].map',
+ }),
]
if (!isDevMode) {
plugins.push(
new CleanWebpackPlugin([resolve(outputDir)], {
root: process.cwd(),
- exclude: dllFiles
+ exclude: dllFiles,
})
)
}
@@ -93,8 +95,8 @@ module.exports = ({
new CopyWebpackPlugin([
{
from: resolve(copyConfig.fromPath),
- to: resolve(copyConfig.toPath) // 找到 dist 目录下的 docs,并放进去
- }
+ to: resolve(copyConfig.toPath), // 找到 dist 目录下的 docs,并放进去
+ },
])
)
}
@@ -103,35 +105,44 @@ module.exports = ({
entry: ['@babel/polyfill', resolve(entryFile)],
output: {
filename: '[name].[hash:8].js',
- path: resolve(outputDir)
+ path: resolve(outputDir),
},
mode: env,
- devtool: isDevMode
- ? 'cheap-module-eval-source-map'
- : 'cheap-module-source-map',
+ devtool: isDevMode ? 'cheap-module-eval-source-map' : 'cheap-module-source-map',
module: {
rules: [
{
test: /\.(js|jsx)$/,
- enforce: 'pre',
- use: {
- loader: 'eslint-loader'
- },
- exclude: /node_modules/
+ // use: ['happypack/loader?id=babel'],
+ loader: 'babel-loader',
+ include: resolve(entryDir),
+ exclude: /node_modules/,
},
{
- test: /(\.js|\.jsx)$/,
- use: ['happypack/loader?id=babel'],
+ test: /\.(ts|tsx)$/,
+ use: [
+ 'happypack/loader?id=babel',
+ {
+ loader: 'awesome-typescript-loader',
+ options: {
+ transpileOnly: true,
+ // compilerOptions: {
+ // module: 'es2015'
+ // },
+ experimentalWatchApi: true,
+ getCustomTransformers: () => ({
+ before: [createMobxTransformer()],
+ }),
+ },
+ },
+ ],
+ include: resolve(entryDir),
exclude: /node_modules/,
- include: resolve(entryDir)
+ // 优化依赖库体积
},
{
test: /\.css$/,
- use: [
- useCssExtract ? MiniCssExtractPlugin.loader : 'style-loader',
- 'css-loader',
- 'postcss-loader'
- ]
+ use: [useCssExtract ? MiniCssExtractPlugin.loader : 'style-loader', 'css-loader', 'postcss-loader'],
},
{
test: /\.less$/,
@@ -143,10 +154,10 @@ module.exports = ({
loader: 'less-loader',
options: {
javascriptEnabled: true,
- sourceMap: true
- }
- }
- ]
+ sourceMap: true,
+ },
+ },
+ ],
},
{
test: /\.scss$/,
@@ -155,40 +166,49 @@ module.exports = ({
'css-loader',
'postcss-loader',
{
- loader: 'scss-loader',
+ loader: 'sass-loader',
options: {
javascriptEnabled: true,
- sourceMap: true
- }
- }
- ]
+ includePaths: [resolve(entryDir, 'common')],
+ sourceMap: true,
+ },
+ },
+ {
+ loader: 'sass-resources-loader', // 全局共用 scss 样式
+ options: {
+ sourceMap: true,
+ resources: resolve(entryDir, 'assets/css/global_vars.scss'),
+ },
+ },
+ ],
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url-loader',
options: Object.assign({}, assetOptions, {
- minetype: 'image/svg+xml'
- })
+ minetype: 'image/svg+xml',
+ }),
},
{
test: /\.(png|jpg|jpeg|gif)(\?v=\d+\.\d+\.\d+)?$/i,
loader: 'url-loader',
- options: assetOptions
+ options: assetOptions,
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
- options: assetOptions
- }
- ]
+ options: assetOptions,
+ },
+ ],
// 'noParse': /jquery/
},
resolve: {
- extensions: ['.js', '.jsx', '.css', '.less', 'scss'],
+ extensions: ['.ts', '.tsx', '.js'],
modules: [resolve(entryDir), resolve('node_modules')],
alias: {
- '@': resolve(entryDir)
- }
+ '@': resolve(entryDir),
+ mobx: resolve('node_modules/mobx/lib/mobx.es6.js'),
+ },
},
optimization: {
splitChunks: {
@@ -198,27 +218,27 @@ module.exports = ({
chunks: 'initial',
minChunks: 2,
maxInitialRequests: 5,
- minSize: 0
+ minSize: 0,
},
vendor: {
test: /node_modules/,
chunks: 'initial',
name: 'vendor',
priority: 10, // 优先
- enforce: true
+ enforce: true,
},
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
- enforce: true
- }
- }
+ enforce: true,
+ },
+ },
},
runtimeChunk: {
- name: 'runtime'
- }
- }
+ name: 'runtime',
+ },
+ },
}
return Object.assign(baseConfig, { plugins })
diff --git a/config/webpack.dev.config.js b/config/webpack.dev.config.js
index ecef7db..a906d75 100644
--- a/config/webpack.dev.config.js
+++ b/config/webpack.dev.config.js
@@ -5,8 +5,6 @@ const options = require('./options')
const getBaseConfig = require('./webpack.base.config')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
-process.env.NODE_ENV = 'development'
-
function getDevConfig(opts) {
return merge(getBaseConfig(opts), {
devServer: {
@@ -14,20 +12,21 @@ function getDevConfig(opts) {
disableHostCheck: true,
compress: true,
historyApiFallback: true, // 不跳转
+ host: '0.0.0.0',
port: 3000,
inline: true, // 全部刷新,当源文件改变时会自动刷新页面
quiet: true, // 不显示 devServer 的 Console 信息,让 FriendlyErrorsWebpackPlugin 取而代之
open: true,
hot: true, // 打开热替换
overlay: {
- errors: true
- }
+ errors: true,
+ },
},
plugins: [
new webpack.HotModuleReplacementPlugin(), // 实现也更新,需要在总 js 入口处判断 module.hot
new webpack.NamedModulesPlugin(),
- new FriendlyErrorsPlugin()
- ]
+ new FriendlyErrorsPlugin(),
+ ],
})
}
diff --git a/config/webpack.dll.config.js b/config/webpack.dll.config.js
index 142d0bc..0b54ffe 100644
--- a/config/webpack.dll.config.js
+++ b/config/webpack.dll.config.js
@@ -9,15 +9,15 @@ function getDllConfig(dll, output) {
filename: '[name].dll.js', // 输出动态连接库的文件名称
path: resolve(output),
libraryTarget: 'var', // 输出方式 默认 'var' 形式赋给变量
- library: '_dll_[name]_[hash]' // 全局变量名称
+ library: '_dll_[name]_[hash]', // 全局变量名称
},
mode: 'production',
plugins: [
new webpack.DllPlugin({
name: '_dll_[name]_[hash]', // 和 library 中一致,输出的 manifest.json 中的 name 值
- path: resolve(output, '[name].manifest.json')
- })
- ]
+ path: resolve(output, '[name].manifest.json'),
+ }),
+ ],
}
}
diff --git a/config/webpack.prod.config.js b/config/webpack.prod.config.js
index e1beedf..1044fdd 100644
--- a/config/webpack.prod.config.js
+++ b/config/webpack.prod.config.js
@@ -5,26 +5,24 @@ const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const options = require('./options')
const getBaseConfig = require('./webpack.base.config')
-process.env.NODE_ENV = 'production'
-
function getProdConfig(opts) {
return merge(getBaseConfig(opts), {
plugins: [
new ParallelUglifyPlugin({
sourceMap: true,
workerCount: 4, // 开启几个子进程去并发的执行压缩
- uglifyJS: {
+ uglifyES: {
output: {
beautify: false, // 不需要格式化
- comments: false // 保留注释
+ comments: false, // 保留注释
},
compress: {
warnings: false, // Uglifyjs 删除没有代码时,不输出警告
drop_console: true,
collapse_vars: true,
- reduce_vars: true
- }
- }
+ reduce_vars: true,
+ },
+ },
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css\.*(?!.*map)/g,
@@ -32,11 +30,11 @@ function getProdConfig(opts) {
cssProcessorOptions: {
discardComments: { removeAll: true },
safe: true, // 避免 cssnano 重新计算 z-index
- autoprefixer: false // 关闭autoprefixer功能 使用postcss的autoprefixer功能
+ autoprefixer: false, // 关闭autoprefixer功能 使用postcss的autoprefixer功能
},
- canPrint: true
+ canPrint: true,
}),
- ]
+ ],
})
}
diff --git a/package.json b/package.json
index 0d862c0..b4209f8 100644
--- a/package.json
+++ b/package.json
@@ -1,83 +1,139 @@
{
- "name": "init-project",
- "version": "0.0.3",
+ "name": "react-lighter",
+ "version": "1.0.0",
"description": "initialize react project",
- "main": "src/index.js",
+ "basePath": "src",
+ "main": "src/index.tsx",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
- "build": "webpack --config config/webpack.prod.config.js --progress",
- "start": "webpack-dev-server --config config/webpack.dev.config.js --progress",
- "lint": "eslint --ext .js src",
+ "start": "cross-env NODE_ENV=development webpack-dev-server --config config/webpack.dev.config.js --progress",
+ "build": "cross-env NODE_ENV=production webpack --config config/webpack.prod.config.js --progress",
+ "lint": "tslint --fix --project .",
"dll": "webpack --config config/webpack.dll.config.js --progress",
- "server": "nodemon server.js"
+ "server": "hs dist"
},
"author": "lawler61",
"license": "ISC",
"pre-commit": [
"lint"
],
- "devDependencies": {
- "@babel/cli": "^7.2.3",
- "@babel/core": "^7.2.2",
- "@babel/plugin-proposal-class-properties": "^7.2.3",
- "@babel/plugin-proposal-decorators": "^7.2.3",
- "@babel/plugin-syntax-dynamic-import": "^7.2.0",
- "@babel/plugin-transform-runtime": "^7.2.0",
- "@babel/preset-env": "^7.2.3",
- "@babel/preset-react": "^7.0.0",
- "add-asset-html-webpack-plugin": "^3.1.2",
- "autoprefixer": "^9.4.3",
- "babel-eslint": "^10.0.1",
- "babel-loader": "^8.0.4",
- "babel-plugin-import": "^1.11.0",
- "clean-webpack-plugin": "^1.0.0",
- "copy-webpack-plugin": "^4.6.0",
- "css-loader": "^2.0.2",
- "cssnano": "^4.1.8",
- "eslint": "^5.10.0",
- "eslint-config-airbnb": "^17.1.0",
- "eslint-config-prettier": "^3.3.0",
- "eslint-loader": "^2.1.1",
- "eslint-plugin-babel": "^5.3.0",
- "eslint-plugin-import": "^2.14.0",
- "eslint-plugin-jsx-a11y": "^6.1.2",
- "eslint-plugin-react": "^7.11.1",
- "extract-text-webpack-plugin": "^4.0.0-beta.0",
- "file-loader": "^3.0.1",
- "friendly-errors-webpack-plugin": "^1.7.0",
- "glob": "^7.1.3",
- "glob-all": "^3.1.0",
- "happypack": "^5.0.0-beta.4",
- "html-webpack-plugin": "^3.2.0",
- "less": "^3.9.0",
- "less-loader": "^4.1.0",
- "mini-css-extract-plugin": "^0.5.0",
- "nodemon": "^1.18.11",
- "optimize-css-assets-webpack-plugin": "^5.0.1",
- "postcss-loader": "^3.0.0",
- "pre-commit": "^1.2.2",
- "purgecss-webpack-plugin": "^1.4.0",
- "purify-css": "^1.2.5",
- "purifycss-webpack": "^0.7.0",
- "scss": "^0.2.4",
- "scss-loader": "^0.0.1",
- "style-loader": "^0.23.1",
- "url-loader": "^1.1.2",
- "webpack": "^4.28.1",
- "webpack-cli": "^3.1.2",
- "webpack-dev-server": "^3.1.11",
- "webpack-merge": "^4.1.5",
- "webpack-parallel-uglify-plugin": "^1.1.0"
+ "keywords": [
+ "react",
+ "mobx",
+ "router",
+ "axios"
+ ],
+ "browserslist": [
+ "> 1%",
+ "last 2 versions",
+ "iOS >= 8",
+ "Android >= 4",
+ "ie >= 9",
+ "Firefox ESR"
+ ],
+ "resolutions": {
+ "browserslist": "4.6.2",
+ "caniuse-lite": "1.0.30000974"
},
"dependencies": {
- "@babel/polyfill": "^7.2.5",
- "ejs": "^2.6.1",
- "prop-types": "^15.6.2",
- "react": "^16.7.0",
- "react-dom": "^16.7.0",
- "react-helmet": "^5.2.0",
- "react-hot-loader": "^4.6.3",
- "react-loadable": "^5.5.0",
- "react-router-dom": "^4.3.1"
+ "@babel/polyfill": "7.2.5",
+ "antd-mobile": "2.2.11",
+ "axios": "0.18.0",
+ "helmet": "3.16.0",
+ "mobx": "5.9.4",
+ "mobx-react": "5.4.3",
+ "qs": "6.7.0",
+ "react": "16.7.0",
+ "react-dom": "16.7.0",
+ "react-helmet": "5.2.0",
+ "react-hot-loader": "4.6.3",
+ "react-loadable": "5.5.0",
+ "react-router-dom": "4.3.1",
+ "styled-components": "4.2.0",
+ "uid": "0.0.2",
+ "zone.js": "0.9.0"
+ },
+ "devDependencies": {
+ "@babel/cli": "7.2.3",
+ "@babel/core": "7.2.2",
+ "@babel/plugin-proposal-class-properties": "7.2.3",
+ "@babel/plugin-proposal-decorators": "7.2.3",
+ "@babel/plugin-syntax-dynamic-import": "7.2.0",
+ "@babel/plugin-transform-runtime": "7.2.0",
+ "@babel/preset-env": "7.2.3",
+ "@babel/preset-react": "7.0.0",
+ "@types/helmet": "0.0.43",
+ "@types/node": "12.0.2",
+ "@types/qs": "6.5.3",
+ "@types/react": "16.8.15",
+ "@types/react-dom": "16.8.4",
+ "@types/react-loadable": "5.5.1",
+ "@types/react-router-dom": "4.3.2",
+ "@types/styled-components": "4.1.14",
+ "add-asset-html-webpack-plugin": "3.1.2",
+ "autoprefixer": "9.4.3",
+ "awesome-typescript-loader": "5.2.1",
+ "babel-eslint": "10.0.1",
+ "babel-loader": "8.0.4",
+ "babel-plugin-import": "1.11.0",
+ "chalk": "2.4.2",
+ "clean-webpack-plugin": "1.0.0",
+ "commander": "2.20.0",
+ "copy-webpack-plugin": "4.6.0",
+ "cross-env": "5.2.0",
+ "css-loader": "2.0.2",
+ "cssnano": "4.1.8",
+ "eslint": "5.10.0",
+ "eslint-config-airbnb": "17.1.0",
+ "eslint-config-prettier": "3.3.0",
+ "eslint-loader": "2.1.1",
+ "eslint-plugin-babel": "5.3.0",
+ "eslint-plugin-import": "2.14.0",
+ "eslint-plugin-jsx-a11y": "6.1.2",
+ "eslint-plugin-react": "7.11.1",
+ "extract-text-webpack-plugin": "4.0.0-beta.0",
+ "file-loader": "3.0.1",
+ "friendly-errors-webpack-plugin": "1.7.0",
+ "fs-extra": "7.0.1",
+ "glob-all": "3.1.0",
+ "happypack": "5.0.0-beta.4",
+ "http-server": "0.11.1",
+ "html-webpack-plugin": "3.2.0",
+ "invariant": "2.2.4",
+ "less": "3.9.0",
+ "less-loader": "4.1.0",
+ "mini-css-extract-plugin": "0.5.0",
+ "node-sass": "4.11.0",
+ "nodemon": "1.18.11",
+ "optimize-css-assets-webpack-plugin": "5.0.1",
+ "postcss-aspect-ratio-mini": "1.0.1",
+ "postcss-import": "12.0.1",
+ "postcss-loader": "3.0.0",
+ "postcss-nested": "4.1.2",
+ "postcss-px-to-viewport": "1.1.0",
+ "postcss-url": "8.0.0",
+ "postcss-viewport-units": "0.1.6",
+ "postcss-write-svg": "3.0.1",
+ "pre-commit": "1.2.2",
+ "precss": "4.0.0",
+ "purgecss-webpack-plugin": "1.4.0",
+ "purify-css": "1.2.5",
+ "purifycss-webpack": "0.7.0",
+ "sass-loader": "7.1.0",
+ "sass-resources-loader": "2.0.0",
+ "scss": "0.2.4",
+ "style-loader": "0.23.1",
+ "sugarss": "2.0.0",
+ "ts-import-plugin": "1.5.5",
+ "tslint": "5.16.0",
+ "tslint-config-prettier": "1.18.0",
+ "tslint-eslint-rules": "5.4.0",
+ "tslint-react": "4.0.0",
+ "typescript": "3.4.3",
+ "url-loader": "1.1.2",
+ "webpack": "4.28.1",
+ "webpack-cli": "3.1.2",
+ "webpack-dev-server": "3.1.11",
+ "webpack-merge": "4.1.5",
+ "webpack-parallel-uglify-plugin": "1.1.0"
}
}
diff --git a/postcss.config.js b/postcss.config.js
index 979ddd9..c1bfdd9 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1,3 +1,33 @@
-module.exports = {
- plugins: [require('autoprefixer')] // eslint-disable-line
+// https://www.jianshu.com/p/477ae5cac982
+// https://mobilesite.github.io/2018/02/05/vm-mobile-layout/
+
+// eslint-disable-next-line
+function getPostcssConfig({ file, options, env }) {
+ return {
+ parser: file.extname === '.sss' ? 'sugarss' : false, // Handles `.css` && '.sss' files dynamically
+ exec: true, // css-in-js
+ plugins: {
+ // 'postcss-import': {}, // css-loader handles @import no need for this plugin in webpack
+ 'postcss-url': {},
+ // 'postcss-cssnext': {}, // includes autoprefixer
+ autoprefixer: {},
+ 'postcss-aspect-ratio-mini': {},
+ 'postcss-viewport-units': {},
+ 'postcss-nested': {},
+ 'postcss-write-svg': { utf8: false },
+ precss: {},
+ 'postcss-px-to-viewport': {
+ viewportWidth: 750,
+ viewportHeight: 1334,
+ unitPrecision: 3,
+ viewportUnit: 'vw',
+ selectorBlackList: ['.ignore'],
+ minPixelValue: 1,
+ mediaQuery: false,
+ },
+ cssnano: env === 'production' ? {} : false,
+ },
+ }
}
+
+module.exports = getPostcssConfig
diff --git a/server.js b/server.js
deleted file mode 100644
index ec471ab..0000000
--- a/server.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const http = require('http')
-const fs = require('fs')
-
-const server = http.createServer((req, res) => {
- const basePath = './dist'
- const filePath = `${basePath}${req.url === '/' ? '/index.html' : req.url}`
-
- fs.readFile(filePath, (err, data) => {
- if (err) {
- res.write('
404!
')
- } else {
- res.write(data)
- }
- res.end()
- })
-})
-
-server.listen(8080, () =>
- console.log('Server running at http://localhost:8080')
-)
diff --git a/src/App.hot.js b/src/App.hot.js
deleted file mode 100644
index cf2ac45..0000000
--- a/src/App.hot.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react'
-import { hot } from 'react-hot-loader'
-import App from './App'
-
-// do not modify this file only if you know what you are doing
-export default hot(module)(() => )
diff --git a/src/App.js b/src/App.js
deleted file mode 100644
index c9c27ea..0000000
--- a/src/App.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-import Hello from './components/Hello/loadable'
-
-import addIcon from './assets/add.svg'
-
-import './index.less'
-
-const App = () => (
-
-
Hello! This is a div from App
-
![add-icon]({addIcon})
-
-
-)
-
-export default App
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..a6fe914
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,22 @@
+import * as React from 'react'
+import { hot } from 'react-hot-loader'
+import { HashRouter as Router } from 'react-router-dom'
+import { configure } from 'mobx'
+import routes from 'routes'
+import { provider } from './mobx/provider'
+import './mobxDependence'
+
+import 'assets/css/global.scss'
+import 'assets/css/font-awesome.min.css'
+
+configure({ enforceActions: 'observed' }) // strict
+
+@provider
+class App extends React.Component {
+ render() {
+ return {routes}
+ }
+}
+
+// do not modify next line only if you know what you are doing
+export default hot(module)(() => )
diff --git a/src/assets/add.svg b/src/assets/add.svg
deleted file mode 100644
index 368405e..0000000
--- a/src/assets/add.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-
diff --git a/src/assets/css/font-awesome.css b/src/assets/css/font-awesome.css
new file mode 100644
index 0000000..ee906a8
--- /dev/null
+++ b/src/assets/css/font-awesome.css
@@ -0,0 +1,2337 @@
+/*!
+ * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+/* FONT PATH
+ * -------------------------- */
+@font-face {
+ font-family: 'FontAwesome';
+ src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');
+ src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+.fa {
+ display: inline-block;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+/* makes the font 33% larger relative to the icon container */
+.fa-lg {
+ font-size: 1.33333333em;
+ line-height: 0.75em;
+ vertical-align: -15%;
+}
+.fa-2x {
+ font-size: 2em;
+}
+.fa-3x {
+ font-size: 3em;
+}
+.fa-4x {
+ font-size: 4em;
+}
+.fa-5x {
+ font-size: 5em;
+}
+.fa-fw {
+ width: 1.28571429em;
+ text-align: center;
+}
+.fa-ul {
+ padding-left: 0;
+ margin-left: 2.14285714em;
+ list-style-type: none;
+}
+.fa-ul > li {
+ position: relative;
+}
+.fa-li {
+ position: absolute;
+ left: -2.14285714em;
+ width: 2.14285714em;
+ top: 0.14285714em;
+ text-align: center;
+}
+.fa-li.fa-lg {
+ left: -1.85714286em;
+}
+.fa-border {
+ padding: .2em .25em .15em;
+ border: solid 0.08em #eeeeee;
+ border-radius: .1em;
+}
+.fa-pull-left {
+ float: left;
+}
+.fa-pull-right {
+ float: right;
+}
+.fa.fa-pull-left {
+ margin-right: .3em;
+}
+.fa.fa-pull-right {
+ margin-left: .3em;
+}
+/* Deprecated as of 4.4.0 */
+.pull-right {
+ float: right;
+}
+.pull-left {
+ float: left;
+}
+.fa.pull-left {
+ margin-right: .3em;
+}
+.fa.pull-right {
+ margin-left: .3em;
+}
+.fa-spin {
+ -webkit-animation: fa-spin 2s infinite linear;
+ animation: fa-spin 2s infinite linear;
+}
+.fa-pulse {
+ -webkit-animation: fa-spin 1s infinite steps(8);
+ animation: fa-spin 1s infinite steps(8);
+}
+@-webkit-keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+.fa-rotate-90 {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
+ -webkit-transform: rotate(90deg);
+ -ms-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+.fa-rotate-180 {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
+ -webkit-transform: rotate(180deg);
+ -ms-transform: rotate(180deg);
+ transform: rotate(180deg);
+}
+.fa-rotate-270 {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
+ -webkit-transform: rotate(270deg);
+ -ms-transform: rotate(270deg);
+ transform: rotate(270deg);
+}
+.fa-flip-horizontal {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
+ -webkit-transform: scale(-1, 1);
+ -ms-transform: scale(-1, 1);
+ transform: scale(-1, 1);
+}
+.fa-flip-vertical {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
+ -webkit-transform: scale(1, -1);
+ -ms-transform: scale(1, -1);
+ transform: scale(1, -1);
+}
+:root .fa-rotate-90,
+:root .fa-rotate-180,
+:root .fa-rotate-270,
+:root .fa-flip-horizontal,
+:root .fa-flip-vertical {
+ filter: none;
+}
+.fa-stack {
+ position: relative;
+ display: inline-block;
+ width: 2em;
+ height: 2em;
+ line-height: 2em;
+ vertical-align: middle;
+}
+.fa-stack-1x,
+.fa-stack-2x {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ text-align: center;
+}
+.fa-stack-1x {
+ line-height: inherit;
+}
+.fa-stack-2x {
+ font-size: 2em;
+}
+.fa-inverse {
+ color: #ffffff;
+}
+/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
+ readers do not read off random characters that represent icons */
+.fa-glass:before {
+ content: "\f000";
+}
+.fa-music:before {
+ content: "\f001";
+}
+.fa-search:before {
+ content: "\f002";
+}
+.fa-envelope-o:before {
+ content: "\f003";
+}
+.fa-heart:before {
+ content: "\f004";
+}
+.fa-star:before {
+ content: "\f005";
+}
+.fa-star-o:before {
+ content: "\f006";
+}
+.fa-user:before {
+ content: "\f007";
+}
+.fa-film:before {
+ content: "\f008";
+}
+.fa-th-large:before {
+ content: "\f009";
+}
+.fa-th:before {
+ content: "\f00a";
+}
+.fa-th-list:before {
+ content: "\f00b";
+}
+.fa-check:before {
+ content: "\f00c";
+}
+.fa-remove:before,
+.fa-close:before,
+.fa-times:before {
+ content: "\f00d";
+}
+.fa-search-plus:before {
+ content: "\f00e";
+}
+.fa-search-minus:before {
+ content: "\f010";
+}
+.fa-power-off:before {
+ content: "\f011";
+}
+.fa-signal:before {
+ content: "\f012";
+}
+.fa-gear:before,
+.fa-cog:before {
+ content: "\f013";
+}
+.fa-trash-o:before {
+ content: "\f014";
+}
+.fa-home:before {
+ content: "\f015";
+}
+.fa-file-o:before {
+ content: "\f016";
+}
+.fa-clock-o:before {
+ content: "\f017";
+}
+.fa-road:before {
+ content: "\f018";
+}
+.fa-download:before {
+ content: "\f019";
+}
+.fa-arrow-circle-o-down:before {
+ content: "\f01a";
+}
+.fa-arrow-circle-o-up:before {
+ content: "\f01b";
+}
+.fa-inbox:before {
+ content: "\f01c";
+}
+.fa-play-circle-o:before {
+ content: "\f01d";
+}
+.fa-rotate-right:before,
+.fa-repeat:before {
+ content: "\f01e";
+}
+.fa-refresh:before {
+ content: "\f021";
+}
+.fa-list-alt:before {
+ content: "\f022";
+}
+.fa-lock:before {
+ content: "\f023";
+}
+.fa-flag:before {
+ content: "\f024";
+}
+.fa-headphones:before {
+ content: "\f025";
+}
+.fa-volume-off:before {
+ content: "\f026";
+}
+.fa-volume-down:before {
+ content: "\f027";
+}
+.fa-volume-up:before {
+ content: "\f028";
+}
+.fa-qrcode:before {
+ content: "\f029";
+}
+.fa-barcode:before {
+ content: "\f02a";
+}
+.fa-tag:before {
+ content: "\f02b";
+}
+.fa-tags:before {
+ content: "\f02c";
+}
+.fa-book:before {
+ content: "\f02d";
+}
+.fa-bookmark:before {
+ content: "\f02e";
+}
+.fa-print:before {
+ content: "\f02f";
+}
+.fa-camera:before {
+ content: "\f030";
+}
+.fa-font:before {
+ content: "\f031";
+}
+.fa-bold:before {
+ content: "\f032";
+}
+.fa-italic:before {
+ content: "\f033";
+}
+.fa-text-height:before {
+ content: "\f034";
+}
+.fa-text-width:before {
+ content: "\f035";
+}
+.fa-align-left:before {
+ content: "\f036";
+}
+.fa-align-center:before {
+ content: "\f037";
+}
+.fa-align-right:before {
+ content: "\f038";
+}
+.fa-align-justify:before {
+ content: "\f039";
+}
+.fa-list:before {
+ content: "\f03a";
+}
+.fa-dedent:before,
+.fa-outdent:before {
+ content: "\f03b";
+}
+.fa-indent:before {
+ content: "\f03c";
+}
+.fa-video-camera:before {
+ content: "\f03d";
+}
+.fa-photo:before,
+.fa-image:before,
+.fa-picture-o:before {
+ content: "\f03e";
+}
+.fa-pencil:before {
+ content: "\f040";
+}
+.fa-map-marker:before {
+ content: "\f041";
+}
+.fa-adjust:before {
+ content: "\f042";
+}
+.fa-tint:before {
+ content: "\f043";
+}
+.fa-edit:before,
+.fa-pencil-square-o:before {
+ content: "\f044";
+}
+.fa-share-square-o:before {
+ content: "\f045";
+}
+.fa-check-square-o:before {
+ content: "\f046";
+}
+.fa-arrows:before {
+ content: "\f047";
+}
+.fa-step-backward:before {
+ content: "\f048";
+}
+.fa-fast-backward:before {
+ content: "\f049";
+}
+.fa-backward:before {
+ content: "\f04a";
+}
+.fa-play:before {
+ content: "\f04b";
+}
+.fa-pause:before {
+ content: "\f04c";
+}
+.fa-stop:before {
+ content: "\f04d";
+}
+.fa-forward:before {
+ content: "\f04e";
+}
+.fa-fast-forward:before {
+ content: "\f050";
+}
+.fa-step-forward:before {
+ content: "\f051";
+}
+.fa-eject:before {
+ content: "\f052";
+}
+.fa-chevron-left:before {
+ content: "\f053";
+}
+.fa-chevron-right:before {
+ content: "\f054";
+}
+.fa-plus-circle:before {
+ content: "\f055";
+}
+.fa-minus-circle:before {
+ content: "\f056";
+}
+.fa-times-circle:before {
+ content: "\f057";
+}
+.fa-check-circle:before {
+ content: "\f058";
+}
+.fa-question-circle:before {
+ content: "\f059";
+}
+.fa-info-circle:before {
+ content: "\f05a";
+}
+.fa-crosshairs:before {
+ content: "\f05b";
+}
+.fa-times-circle-o:before {
+ content: "\f05c";
+}
+.fa-check-circle-o:before {
+ content: "\f05d";
+}
+.fa-ban:before {
+ content: "\f05e";
+}
+.fa-arrow-left:before {
+ content: "\f060";
+}
+.fa-arrow-right:before {
+ content: "\f061";
+}
+.fa-arrow-up:before {
+ content: "\f062";
+}
+.fa-arrow-down:before {
+ content: "\f063";
+}
+.fa-mail-forward:before,
+.fa-share:before {
+ content: "\f064";
+}
+.fa-expand:before {
+ content: "\f065";
+}
+.fa-compress:before {
+ content: "\f066";
+}
+.fa-plus:before {
+ content: "\f067";
+}
+.fa-minus:before {
+ content: "\f068";
+}
+.fa-asterisk:before {
+ content: "\f069";
+}
+.fa-exclamation-circle:before {
+ content: "\f06a";
+}
+.fa-gift:before {
+ content: "\f06b";
+}
+.fa-leaf:before {
+ content: "\f06c";
+}
+.fa-fire:before {
+ content: "\f06d";
+}
+.fa-eye:before {
+ content: "\f06e";
+}
+.fa-eye-slash:before {
+ content: "\f070";
+}
+.fa-warning:before,
+.fa-exclamation-triangle:before {
+ content: "\f071";
+}
+.fa-plane:before {
+ content: "\f072";
+}
+.fa-calendar:before {
+ content: "\f073";
+}
+.fa-random:before {
+ content: "\f074";
+}
+.fa-comment:before {
+ content: "\f075";
+}
+.fa-magnet:before {
+ content: "\f076";
+}
+.fa-chevron-up:before {
+ content: "\f077";
+}
+.fa-chevron-down:before {
+ content: "\f078";
+}
+.fa-retweet:before {
+ content: "\f079";
+}
+.fa-shopping-cart:before {
+ content: "\f07a";
+}
+.fa-folder:before {
+ content: "\f07b";
+}
+.fa-folder-open:before {
+ content: "\f07c";
+}
+.fa-arrows-v:before {
+ content: "\f07d";
+}
+.fa-arrows-h:before {
+ content: "\f07e";
+}
+.fa-bar-chart-o:before,
+.fa-bar-chart:before {
+ content: "\f080";
+}
+.fa-twitter-square:before {
+ content: "\f081";
+}
+.fa-facebook-square:before {
+ content: "\f082";
+}
+.fa-camera-retro:before {
+ content: "\f083";
+}
+.fa-key:before {
+ content: "\f084";
+}
+.fa-gears:before,
+.fa-cogs:before {
+ content: "\f085";
+}
+.fa-comments:before {
+ content: "\f086";
+}
+.fa-thumbs-o-up:before {
+ content: "\f087";
+}
+.fa-thumbs-o-down:before {
+ content: "\f088";
+}
+.fa-star-half:before {
+ content: "\f089";
+}
+.fa-heart-o:before {
+ content: "\f08a";
+}
+.fa-sign-out:before {
+ content: "\f08b";
+}
+.fa-linkedin-square:before {
+ content: "\f08c";
+}
+.fa-thumb-tack:before {
+ content: "\f08d";
+}
+.fa-external-link:before {
+ content: "\f08e";
+}
+.fa-sign-in:before {
+ content: "\f090";
+}
+.fa-trophy:before {
+ content: "\f091";
+}
+.fa-github-square:before {
+ content: "\f092";
+}
+.fa-upload:before {
+ content: "\f093";
+}
+.fa-lemon-o:before {
+ content: "\f094";
+}
+.fa-phone:before {
+ content: "\f095";
+}
+.fa-square-o:before {
+ content: "\f096";
+}
+.fa-bookmark-o:before {
+ content: "\f097";
+}
+.fa-phone-square:before {
+ content: "\f098";
+}
+.fa-twitter:before {
+ content: "\f099";
+}
+.fa-facebook-f:before,
+.fa-facebook:before {
+ content: "\f09a";
+}
+.fa-github:before {
+ content: "\f09b";
+}
+.fa-unlock:before {
+ content: "\f09c";
+}
+.fa-credit-card:before {
+ content: "\f09d";
+}
+.fa-feed:before,
+.fa-rss:before {
+ content: "\f09e";
+}
+.fa-hdd-o:before {
+ content: "\f0a0";
+}
+.fa-bullhorn:before {
+ content: "\f0a1";
+}
+.fa-bell:before {
+ content: "\f0f3";
+}
+.fa-certificate:before {
+ content: "\f0a3";
+}
+.fa-hand-o-right:before {
+ content: "\f0a4";
+}
+.fa-hand-o-left:before {
+ content: "\f0a5";
+}
+.fa-hand-o-up:before {
+ content: "\f0a6";
+}
+.fa-hand-o-down:before {
+ content: "\f0a7";
+}
+.fa-arrow-circle-left:before {
+ content: "\f0a8";
+}
+.fa-arrow-circle-right:before {
+ content: "\f0a9";
+}
+.fa-arrow-circle-up:before {
+ content: "\f0aa";
+}
+.fa-arrow-circle-down:before {
+ content: "\f0ab";
+}
+.fa-globe:before {
+ content: "\f0ac";
+}
+.fa-wrench:before {
+ content: "\f0ad";
+}
+.fa-tasks:before {
+ content: "\f0ae";
+}
+.fa-filter:before {
+ content: "\f0b0";
+}
+.fa-briefcase:before {
+ content: "\f0b1";
+}
+.fa-arrows-alt:before {
+ content: "\f0b2";
+}
+.fa-group:before,
+.fa-users:before {
+ content: "\f0c0";
+}
+.fa-chain:before,
+.fa-link:before {
+ content: "\f0c1";
+}
+.fa-cloud:before {
+ content: "\f0c2";
+}
+.fa-flask:before {
+ content: "\f0c3";
+}
+.fa-cut:before,
+.fa-scissors:before {
+ content: "\f0c4";
+}
+.fa-copy:before,
+.fa-files-o:before {
+ content: "\f0c5";
+}
+.fa-paperclip:before {
+ content: "\f0c6";
+}
+.fa-save:before,
+.fa-floppy-o:before {
+ content: "\f0c7";
+}
+.fa-square:before {
+ content: "\f0c8";
+}
+.fa-navicon:before,
+.fa-reorder:before,
+.fa-bars:before {
+ content: "\f0c9";
+}
+.fa-list-ul:before {
+ content: "\f0ca";
+}
+.fa-list-ol:before {
+ content: "\f0cb";
+}
+.fa-strikethrough:before {
+ content: "\f0cc";
+}
+.fa-underline:before {
+ content: "\f0cd";
+}
+.fa-table:before {
+ content: "\f0ce";
+}
+.fa-magic:before {
+ content: "\f0d0";
+}
+.fa-truck:before {
+ content: "\f0d1";
+}
+.fa-pinterest:before {
+ content: "\f0d2";
+}
+.fa-pinterest-square:before {
+ content: "\f0d3";
+}
+.fa-google-plus-square:before {
+ content: "\f0d4";
+}
+.fa-google-plus:before {
+ content: "\f0d5";
+}
+.fa-money:before {
+ content: "\f0d6";
+}
+.fa-caret-down:before {
+ content: "\f0d7";
+}
+.fa-caret-up:before {
+ content: "\f0d8";
+}
+.fa-caret-left:before {
+ content: "\f0d9";
+}
+.fa-caret-right:before {
+ content: "\f0da";
+}
+.fa-columns:before {
+ content: "\f0db";
+}
+.fa-unsorted:before,
+.fa-sort:before {
+ content: "\f0dc";
+}
+.fa-sort-down:before,
+.fa-sort-desc:before {
+ content: "\f0dd";
+}
+.fa-sort-up:before,
+.fa-sort-asc:before {
+ content: "\f0de";
+}
+.fa-envelope:before {
+ content: "\f0e0";
+}
+.fa-linkedin:before {
+ content: "\f0e1";
+}
+.fa-rotate-left:before,
+.fa-undo:before {
+ content: "\f0e2";
+}
+.fa-legal:before,
+.fa-gavel:before {
+ content: "\f0e3";
+}
+.fa-dashboard:before,
+.fa-tachometer:before {
+ content: "\f0e4";
+}
+.fa-comment-o:before {
+ content: "\f0e5";
+}
+.fa-comments-o:before {
+ content: "\f0e6";
+}
+.fa-flash:before,
+.fa-bolt:before {
+ content: "\f0e7";
+}
+.fa-sitemap:before {
+ content: "\f0e8";
+}
+.fa-umbrella:before {
+ content: "\f0e9";
+}
+.fa-paste:before,
+.fa-clipboard:before {
+ content: "\f0ea";
+}
+.fa-lightbulb-o:before {
+ content: "\f0eb";
+}
+.fa-exchange:before {
+ content: "\f0ec";
+}
+.fa-cloud-download:before {
+ content: "\f0ed";
+}
+.fa-cloud-upload:before {
+ content: "\f0ee";
+}
+.fa-user-md:before {
+ content: "\f0f0";
+}
+.fa-stethoscope:before {
+ content: "\f0f1";
+}
+.fa-suitcase:before {
+ content: "\f0f2";
+}
+.fa-bell-o:before {
+ content: "\f0a2";
+}
+.fa-coffee:before {
+ content: "\f0f4";
+}
+.fa-cutlery:before {
+ content: "\f0f5";
+}
+.fa-file-text-o:before {
+ content: "\f0f6";
+}
+.fa-building-o:before {
+ content: "\f0f7";
+}
+.fa-hospital-o:before {
+ content: "\f0f8";
+}
+.fa-ambulance:before {
+ content: "\f0f9";
+}
+.fa-medkit:before {
+ content: "\f0fa";
+}
+.fa-fighter-jet:before {
+ content: "\f0fb";
+}
+.fa-beer:before {
+ content: "\f0fc";
+}
+.fa-h-square:before {
+ content: "\f0fd";
+}
+.fa-plus-square:before {
+ content: "\f0fe";
+}
+.fa-angle-double-left:before {
+ content: "\f100";
+}
+.fa-angle-double-right:before {
+ content: "\f101";
+}
+.fa-angle-double-up:before {
+ content: "\f102";
+}
+.fa-angle-double-down:before {
+ content: "\f103";
+}
+.fa-angle-left:before {
+ content: "\f104";
+}
+.fa-angle-right:before {
+ content: "\f105";
+}
+.fa-angle-up:before {
+ content: "\f106";
+}
+.fa-angle-down:before {
+ content: "\f107";
+}
+.fa-desktop:before {
+ content: "\f108";
+}
+.fa-laptop:before {
+ content: "\f109";
+}
+.fa-tablet:before {
+ content: "\f10a";
+}
+.fa-mobile-phone:before,
+.fa-mobile:before {
+ content: "\f10b";
+}
+.fa-circle-o:before {
+ content: "\f10c";
+}
+.fa-quote-left:before {
+ content: "\f10d";
+}
+.fa-quote-right:before {
+ content: "\f10e";
+}
+.fa-spinner:before {
+ content: "\f110";
+}
+.fa-circle:before {
+ content: "\f111";
+}
+.fa-mail-reply:before,
+.fa-reply:before {
+ content: "\f112";
+}
+.fa-github-alt:before {
+ content: "\f113";
+}
+.fa-folder-o:before {
+ content: "\f114";
+}
+.fa-folder-open-o:before {
+ content: "\f115";
+}
+.fa-smile-o:before {
+ content: "\f118";
+}
+.fa-frown-o:before {
+ content: "\f119";
+}
+.fa-meh-o:before {
+ content: "\f11a";
+}
+.fa-gamepad:before {
+ content: "\f11b";
+}
+.fa-keyboard-o:before {
+ content: "\f11c";
+}
+.fa-flag-o:before {
+ content: "\f11d";
+}
+.fa-flag-checkered:before {
+ content: "\f11e";
+}
+.fa-terminal:before {
+ content: "\f120";
+}
+.fa-code:before {
+ content: "\f121";
+}
+.fa-mail-reply-all:before,
+.fa-reply-all:before {
+ content: "\f122";
+}
+.fa-star-half-empty:before,
+.fa-star-half-full:before,
+.fa-star-half-o:before {
+ content: "\f123";
+}
+.fa-location-arrow:before {
+ content: "\f124";
+}
+.fa-crop:before {
+ content: "\f125";
+}
+.fa-code-fork:before {
+ content: "\f126";
+}
+.fa-unlink:before,
+.fa-chain-broken:before {
+ content: "\f127";
+}
+.fa-question:before {
+ content: "\f128";
+}
+.fa-info:before {
+ content: "\f129";
+}
+.fa-exclamation:before {
+ content: "\f12a";
+}
+.fa-superscript:before {
+ content: "\f12b";
+}
+.fa-subscript:before {
+ content: "\f12c";
+}
+.fa-eraser:before {
+ content: "\f12d";
+}
+.fa-puzzle-piece:before {
+ content: "\f12e";
+}
+.fa-microphone:before {
+ content: "\f130";
+}
+.fa-microphone-slash:before {
+ content: "\f131";
+}
+.fa-shield:before {
+ content: "\f132";
+}
+.fa-calendar-o:before {
+ content: "\f133";
+}
+.fa-fire-extinguisher:before {
+ content: "\f134";
+}
+.fa-rocket:before {
+ content: "\f135";
+}
+.fa-maxcdn:before {
+ content: "\f136";
+}
+.fa-chevron-circle-left:before {
+ content: "\f137";
+}
+.fa-chevron-circle-right:before {
+ content: "\f138";
+}
+.fa-chevron-circle-up:before {
+ content: "\f139";
+}
+.fa-chevron-circle-down:before {
+ content: "\f13a";
+}
+.fa-html5:before {
+ content: "\f13b";
+}
+.fa-css3:before {
+ content: "\f13c";
+}
+.fa-anchor:before {
+ content: "\f13d";
+}
+.fa-unlock-alt:before {
+ content: "\f13e";
+}
+.fa-bullseye:before {
+ content: "\f140";
+}
+.fa-ellipsis-h:before {
+ content: "\f141";
+}
+.fa-ellipsis-v:before {
+ content: "\f142";
+}
+.fa-rss-square:before {
+ content: "\f143";
+}
+.fa-play-circle:before {
+ content: "\f144";
+}
+.fa-ticket:before {
+ content: "\f145";
+}
+.fa-minus-square:before {
+ content: "\f146";
+}
+.fa-minus-square-o:before {
+ content: "\f147";
+}
+.fa-level-up:before {
+ content: "\f148";
+}
+.fa-level-down:before {
+ content: "\f149";
+}
+.fa-check-square:before {
+ content: "\f14a";
+}
+.fa-pencil-square:before {
+ content: "\f14b";
+}
+.fa-external-link-square:before {
+ content: "\f14c";
+}
+.fa-share-square:before {
+ content: "\f14d";
+}
+.fa-compass:before {
+ content: "\f14e";
+}
+.fa-toggle-down:before,
+.fa-caret-square-o-down:before {
+ content: "\f150";
+}
+.fa-toggle-up:before,
+.fa-caret-square-o-up:before {
+ content: "\f151";
+}
+.fa-toggle-right:before,
+.fa-caret-square-o-right:before {
+ content: "\f152";
+}
+.fa-euro:before,
+.fa-eur:before {
+ content: "\f153";
+}
+.fa-gbp:before {
+ content: "\f154";
+}
+.fa-dollar:before,
+.fa-usd:before {
+ content: "\f155";
+}
+.fa-rupee:before,
+.fa-inr:before {
+ content: "\f156";
+}
+.fa-cny:before,
+.fa-rmb:before,
+.fa-yen:before,
+.fa-jpy:before {
+ content: "\f157";
+}
+.fa-ruble:before,
+.fa-rouble:before,
+.fa-rub:before {
+ content: "\f158";
+}
+.fa-won:before,
+.fa-krw:before {
+ content: "\f159";
+}
+.fa-bitcoin:before,
+.fa-btc:before {
+ content: "\f15a";
+}
+.fa-file:before {
+ content: "\f15b";
+}
+.fa-file-text:before {
+ content: "\f15c";
+}
+.fa-sort-alpha-asc:before {
+ content: "\f15d";
+}
+.fa-sort-alpha-desc:before {
+ content: "\f15e";
+}
+.fa-sort-amount-asc:before {
+ content: "\f160";
+}
+.fa-sort-amount-desc:before {
+ content: "\f161";
+}
+.fa-sort-numeric-asc:before {
+ content: "\f162";
+}
+.fa-sort-numeric-desc:before {
+ content: "\f163";
+}
+.fa-thumbs-up:before {
+ content: "\f164";
+}
+.fa-thumbs-down:before {
+ content: "\f165";
+}
+.fa-youtube-square:before {
+ content: "\f166";
+}
+.fa-youtube:before {
+ content: "\f167";
+}
+.fa-xing:before {
+ content: "\f168";
+}
+.fa-xing-square:before {
+ content: "\f169";
+}
+.fa-youtube-play:before {
+ content: "\f16a";
+}
+.fa-dropbox:before {
+ content: "\f16b";
+}
+.fa-stack-overflow:before {
+ content: "\f16c";
+}
+.fa-instagram:before {
+ content: "\f16d";
+}
+.fa-flickr:before {
+ content: "\f16e";
+}
+.fa-adn:before {
+ content: "\f170";
+}
+.fa-bitbucket:before {
+ content: "\f171";
+}
+.fa-bitbucket-square:before {
+ content: "\f172";
+}
+.fa-tumblr:before {
+ content: "\f173";
+}
+.fa-tumblr-square:before {
+ content: "\f174";
+}
+.fa-long-arrow-down:before {
+ content: "\f175";
+}
+.fa-long-arrow-up:before {
+ content: "\f176";
+}
+.fa-long-arrow-left:before {
+ content: "\f177";
+}
+.fa-long-arrow-right:before {
+ content: "\f178";
+}
+.fa-apple:before {
+ content: "\f179";
+}
+.fa-windows:before {
+ content: "\f17a";
+}
+.fa-android:before {
+ content: "\f17b";
+}
+.fa-linux:before {
+ content: "\f17c";
+}
+.fa-dribbble:before {
+ content: "\f17d";
+}
+.fa-skype:before {
+ content: "\f17e";
+}
+.fa-foursquare:before {
+ content: "\f180";
+}
+.fa-trello:before {
+ content: "\f181";
+}
+.fa-female:before {
+ content: "\f182";
+}
+.fa-male:before {
+ content: "\f183";
+}
+.fa-gittip:before,
+.fa-gratipay:before {
+ content: "\f184";
+}
+.fa-sun-o:before {
+ content: "\f185";
+}
+.fa-moon-o:before {
+ content: "\f186";
+}
+.fa-archive:before {
+ content: "\f187";
+}
+.fa-bug:before {
+ content: "\f188";
+}
+.fa-vk:before {
+ content: "\f189";
+}
+.fa-weibo:before {
+ content: "\f18a";
+}
+.fa-renren:before {
+ content: "\f18b";
+}
+.fa-pagelines:before {
+ content: "\f18c";
+}
+.fa-stack-exchange:before {
+ content: "\f18d";
+}
+.fa-arrow-circle-o-right:before {
+ content: "\f18e";
+}
+.fa-arrow-circle-o-left:before {
+ content: "\f190";
+}
+.fa-toggle-left:before,
+.fa-caret-square-o-left:before {
+ content: "\f191";
+}
+.fa-dot-circle-o:before {
+ content: "\f192";
+}
+.fa-wheelchair:before {
+ content: "\f193";
+}
+.fa-vimeo-square:before {
+ content: "\f194";
+}
+.fa-turkish-lira:before,
+.fa-try:before {
+ content: "\f195";
+}
+.fa-plus-square-o:before {
+ content: "\f196";
+}
+.fa-space-shuttle:before {
+ content: "\f197";
+}
+.fa-slack:before {
+ content: "\f198";
+}
+.fa-envelope-square:before {
+ content: "\f199";
+}
+.fa-wordpress:before {
+ content: "\f19a";
+}
+.fa-openid:before {
+ content: "\f19b";
+}
+.fa-institution:before,
+.fa-bank:before,
+.fa-university:before {
+ content: "\f19c";
+}
+.fa-mortar-board:before,
+.fa-graduation-cap:before {
+ content: "\f19d";
+}
+.fa-yahoo:before {
+ content: "\f19e";
+}
+.fa-google:before {
+ content: "\f1a0";
+}
+.fa-reddit:before {
+ content: "\f1a1";
+}
+.fa-reddit-square:before {
+ content: "\f1a2";
+}
+.fa-stumbleupon-circle:before {
+ content: "\f1a3";
+}
+.fa-stumbleupon:before {
+ content: "\f1a4";
+}
+.fa-delicious:before {
+ content: "\f1a5";
+}
+.fa-digg:before {
+ content: "\f1a6";
+}
+.fa-pied-piper-pp:before {
+ content: "\f1a7";
+}
+.fa-pied-piper-alt:before {
+ content: "\f1a8";
+}
+.fa-drupal:before {
+ content: "\f1a9";
+}
+.fa-joomla:before {
+ content: "\f1aa";
+}
+.fa-language:before {
+ content: "\f1ab";
+}
+.fa-fax:before {
+ content: "\f1ac";
+}
+.fa-building:before {
+ content: "\f1ad";
+}
+.fa-child:before {
+ content: "\f1ae";
+}
+.fa-paw:before {
+ content: "\f1b0";
+}
+.fa-spoon:before {
+ content: "\f1b1";
+}
+.fa-cube:before {
+ content: "\f1b2";
+}
+.fa-cubes:before {
+ content: "\f1b3";
+}
+.fa-behance:before {
+ content: "\f1b4";
+}
+.fa-behance-square:before {
+ content: "\f1b5";
+}
+.fa-steam:before {
+ content: "\f1b6";
+}
+.fa-steam-square:before {
+ content: "\f1b7";
+}
+.fa-recycle:before {
+ content: "\f1b8";
+}
+.fa-automobile:before,
+.fa-car:before {
+ content: "\f1b9";
+}
+.fa-cab:before,
+.fa-taxi:before {
+ content: "\f1ba";
+}
+.fa-tree:before {
+ content: "\f1bb";
+}
+.fa-spotify:before {
+ content: "\f1bc";
+}
+.fa-deviantart:before {
+ content: "\f1bd";
+}
+.fa-soundcloud:before {
+ content: "\f1be";
+}
+.fa-database:before {
+ content: "\f1c0";
+}
+.fa-file-pdf-o:before {
+ content: "\f1c1";
+}
+.fa-file-word-o:before {
+ content: "\f1c2";
+}
+.fa-file-excel-o:before {
+ content: "\f1c3";
+}
+.fa-file-powerpoint-o:before {
+ content: "\f1c4";
+}
+.fa-file-photo-o:before,
+.fa-file-picture-o:before,
+.fa-file-image-o:before {
+ content: "\f1c5";
+}
+.fa-file-zip-o:before,
+.fa-file-archive-o:before {
+ content: "\f1c6";
+}
+.fa-file-sound-o:before,
+.fa-file-audio-o:before {
+ content: "\f1c7";
+}
+.fa-file-movie-o:before,
+.fa-file-video-o:before {
+ content: "\f1c8";
+}
+.fa-file-code-o:before {
+ content: "\f1c9";
+}
+.fa-vine:before {
+ content: "\f1ca";
+}
+.fa-codepen:before {
+ content: "\f1cb";
+}
+.fa-jsfiddle:before {
+ content: "\f1cc";
+}
+.fa-life-bouy:before,
+.fa-life-buoy:before,
+.fa-life-saver:before,
+.fa-support:before,
+.fa-life-ring:before {
+ content: "\f1cd";
+}
+.fa-circle-o-notch:before {
+ content: "\f1ce";
+}
+.fa-ra:before,
+.fa-resistance:before,
+.fa-rebel:before {
+ content: "\f1d0";
+}
+.fa-ge:before,
+.fa-empire:before {
+ content: "\f1d1";
+}
+.fa-git-square:before {
+ content: "\f1d2";
+}
+.fa-git:before {
+ content: "\f1d3";
+}
+.fa-y-combinator-square:before,
+.fa-yc-square:before,
+.fa-hacker-news:before {
+ content: "\f1d4";
+}
+.fa-tencent-weibo:before {
+ content: "\f1d5";
+}
+.fa-qq:before {
+ content: "\f1d6";
+}
+.fa-wechat:before,
+.fa-weixin:before {
+ content: "\f1d7";
+}
+.fa-send:before,
+.fa-paper-plane:before {
+ content: "\f1d8";
+}
+.fa-send-o:before,
+.fa-paper-plane-o:before {
+ content: "\f1d9";
+}
+.fa-history:before {
+ content: "\f1da";
+}
+.fa-circle-thin:before {
+ content: "\f1db";
+}
+.fa-header:before {
+ content: "\f1dc";
+}
+.fa-paragraph:before {
+ content: "\f1dd";
+}
+.fa-sliders:before {
+ content: "\f1de";
+}
+.fa-share-alt:before {
+ content: "\f1e0";
+}
+.fa-share-alt-square:before {
+ content: "\f1e1";
+}
+.fa-bomb:before {
+ content: "\f1e2";
+}
+.fa-soccer-ball-o:before,
+.fa-futbol-o:before {
+ content: "\f1e3";
+}
+.fa-tty:before {
+ content: "\f1e4";
+}
+.fa-binoculars:before {
+ content: "\f1e5";
+}
+.fa-plug:before {
+ content: "\f1e6";
+}
+.fa-slideshare:before {
+ content: "\f1e7";
+}
+.fa-twitch:before {
+ content: "\f1e8";
+}
+.fa-yelp:before {
+ content: "\f1e9";
+}
+.fa-newspaper-o:before {
+ content: "\f1ea";
+}
+.fa-wifi:before {
+ content: "\f1eb";
+}
+.fa-calculator:before {
+ content: "\f1ec";
+}
+.fa-paypal:before {
+ content: "\f1ed";
+}
+.fa-google-wallet:before {
+ content: "\f1ee";
+}
+.fa-cc-visa:before {
+ content: "\f1f0";
+}
+.fa-cc-mastercard:before {
+ content: "\f1f1";
+}
+.fa-cc-discover:before {
+ content: "\f1f2";
+}
+.fa-cc-amex:before {
+ content: "\f1f3";
+}
+.fa-cc-paypal:before {
+ content: "\f1f4";
+}
+.fa-cc-stripe:before {
+ content: "\f1f5";
+}
+.fa-bell-slash:before {
+ content: "\f1f6";
+}
+.fa-bell-slash-o:before {
+ content: "\f1f7";
+}
+.fa-trash:before {
+ content: "\f1f8";
+}
+.fa-copyright:before {
+ content: "\f1f9";
+}
+.fa-at:before {
+ content: "\f1fa";
+}
+.fa-eyedropper:before {
+ content: "\f1fb";
+}
+.fa-paint-brush:before {
+ content: "\f1fc";
+}
+.fa-birthday-cake:before {
+ content: "\f1fd";
+}
+.fa-area-chart:before {
+ content: "\f1fe";
+}
+.fa-pie-chart:before {
+ content: "\f200";
+}
+.fa-line-chart:before {
+ content: "\f201";
+}
+.fa-lastfm:before {
+ content: "\f202";
+}
+.fa-lastfm-square:before {
+ content: "\f203";
+}
+.fa-toggle-off:before {
+ content: "\f204";
+}
+.fa-toggle-on:before {
+ content: "\f205";
+}
+.fa-bicycle:before {
+ content: "\f206";
+}
+.fa-bus:before {
+ content: "\f207";
+}
+.fa-ioxhost:before {
+ content: "\f208";
+}
+.fa-angellist:before {
+ content: "\f209";
+}
+.fa-cc:before {
+ content: "\f20a";
+}
+.fa-shekel:before,
+.fa-sheqel:before,
+.fa-ils:before {
+ content: "\f20b";
+}
+.fa-meanpath:before {
+ content: "\f20c";
+}
+.fa-buysellads:before {
+ content: "\f20d";
+}
+.fa-connectdevelop:before {
+ content: "\f20e";
+}
+.fa-dashcube:before {
+ content: "\f210";
+}
+.fa-forumbee:before {
+ content: "\f211";
+}
+.fa-leanpub:before {
+ content: "\f212";
+}
+.fa-sellsy:before {
+ content: "\f213";
+}
+.fa-shirtsinbulk:before {
+ content: "\f214";
+}
+.fa-simplybuilt:before {
+ content: "\f215";
+}
+.fa-skyatlas:before {
+ content: "\f216";
+}
+.fa-cart-plus:before {
+ content: "\f217";
+}
+.fa-cart-arrow-down:before {
+ content: "\f218";
+}
+.fa-diamond:before {
+ content: "\f219";
+}
+.fa-ship:before {
+ content: "\f21a";
+}
+.fa-user-secret:before {
+ content: "\f21b";
+}
+.fa-motorcycle:before {
+ content: "\f21c";
+}
+.fa-street-view:before {
+ content: "\f21d";
+}
+.fa-heartbeat:before {
+ content: "\f21e";
+}
+.fa-venus:before {
+ content: "\f221";
+}
+.fa-mars:before {
+ content: "\f222";
+}
+.fa-mercury:before {
+ content: "\f223";
+}
+.fa-intersex:before,
+.fa-transgender:before {
+ content: "\f224";
+}
+.fa-transgender-alt:before {
+ content: "\f225";
+}
+.fa-venus-double:before {
+ content: "\f226";
+}
+.fa-mars-double:before {
+ content: "\f227";
+}
+.fa-venus-mars:before {
+ content: "\f228";
+}
+.fa-mars-stroke:before {
+ content: "\f229";
+}
+.fa-mars-stroke-v:before {
+ content: "\f22a";
+}
+.fa-mars-stroke-h:before {
+ content: "\f22b";
+}
+.fa-neuter:before {
+ content: "\f22c";
+}
+.fa-genderless:before {
+ content: "\f22d";
+}
+.fa-facebook-official:before {
+ content: "\f230";
+}
+.fa-pinterest-p:before {
+ content: "\f231";
+}
+.fa-whatsapp:before {
+ content: "\f232";
+}
+.fa-server:before {
+ content: "\f233";
+}
+.fa-user-plus:before {
+ content: "\f234";
+}
+.fa-user-times:before {
+ content: "\f235";
+}
+.fa-hotel:before,
+.fa-bed:before {
+ content: "\f236";
+}
+.fa-viacoin:before {
+ content: "\f237";
+}
+.fa-train:before {
+ content: "\f238";
+}
+.fa-subway:before {
+ content: "\f239";
+}
+.fa-medium:before {
+ content: "\f23a";
+}
+.fa-yc:before,
+.fa-y-combinator:before {
+ content: "\f23b";
+}
+.fa-optin-monster:before {
+ content: "\f23c";
+}
+.fa-opencart:before {
+ content: "\f23d";
+}
+.fa-expeditedssl:before {
+ content: "\f23e";
+}
+.fa-battery-4:before,
+.fa-battery:before,
+.fa-battery-full:before {
+ content: "\f240";
+}
+.fa-battery-3:before,
+.fa-battery-three-quarters:before {
+ content: "\f241";
+}
+.fa-battery-2:before,
+.fa-battery-half:before {
+ content: "\f242";
+}
+.fa-battery-1:before,
+.fa-battery-quarter:before {
+ content: "\f243";
+}
+.fa-battery-0:before,
+.fa-battery-empty:before {
+ content: "\f244";
+}
+.fa-mouse-pointer:before {
+ content: "\f245";
+}
+.fa-i-cursor:before {
+ content: "\f246";
+}
+.fa-object-group:before {
+ content: "\f247";
+}
+.fa-object-ungroup:before {
+ content: "\f248";
+}
+.fa-sticky-note:before {
+ content: "\f249";
+}
+.fa-sticky-note-o:before {
+ content: "\f24a";
+}
+.fa-cc-jcb:before {
+ content: "\f24b";
+}
+.fa-cc-diners-club:before {
+ content: "\f24c";
+}
+.fa-clone:before {
+ content: "\f24d";
+}
+.fa-balance-scale:before {
+ content: "\f24e";
+}
+.fa-hourglass-o:before {
+ content: "\f250";
+}
+.fa-hourglass-1:before,
+.fa-hourglass-start:before {
+ content: "\f251";
+}
+.fa-hourglass-2:before,
+.fa-hourglass-half:before {
+ content: "\f252";
+}
+.fa-hourglass-3:before,
+.fa-hourglass-end:before {
+ content: "\f253";
+}
+.fa-hourglass:before {
+ content: "\f254";
+}
+.fa-hand-grab-o:before,
+.fa-hand-rock-o:before {
+ content: "\f255";
+}
+.fa-hand-stop-o:before,
+.fa-hand-paper-o:before {
+ content: "\f256";
+}
+.fa-hand-scissors-o:before {
+ content: "\f257";
+}
+.fa-hand-lizard-o:before {
+ content: "\f258";
+}
+.fa-hand-spock-o:before {
+ content: "\f259";
+}
+.fa-hand-pointer-o:before {
+ content: "\f25a";
+}
+.fa-hand-peace-o:before {
+ content: "\f25b";
+}
+.fa-trademark:before {
+ content: "\f25c";
+}
+.fa-registered:before {
+ content: "\f25d";
+}
+.fa-creative-commons:before {
+ content: "\f25e";
+}
+.fa-gg:before {
+ content: "\f260";
+}
+.fa-gg-circle:before {
+ content: "\f261";
+}
+.fa-tripadvisor:before {
+ content: "\f262";
+}
+.fa-odnoklassniki:before {
+ content: "\f263";
+}
+.fa-odnoklassniki-square:before {
+ content: "\f264";
+}
+.fa-get-pocket:before {
+ content: "\f265";
+}
+.fa-wikipedia-w:before {
+ content: "\f266";
+}
+.fa-safari:before {
+ content: "\f267";
+}
+.fa-chrome:before {
+ content: "\f268";
+}
+.fa-firefox:before {
+ content: "\f269";
+}
+.fa-opera:before {
+ content: "\f26a";
+}
+.fa-internet-explorer:before {
+ content: "\f26b";
+}
+.fa-tv:before,
+.fa-television:before {
+ content: "\f26c";
+}
+.fa-contao:before {
+ content: "\f26d";
+}
+.fa-500px:before {
+ content: "\f26e";
+}
+.fa-amazon:before {
+ content: "\f270";
+}
+.fa-calendar-plus-o:before {
+ content: "\f271";
+}
+.fa-calendar-minus-o:before {
+ content: "\f272";
+}
+.fa-calendar-times-o:before {
+ content: "\f273";
+}
+.fa-calendar-check-o:before {
+ content: "\f274";
+}
+.fa-industry:before {
+ content: "\f275";
+}
+.fa-map-pin:before {
+ content: "\f276";
+}
+.fa-map-signs:before {
+ content: "\f277";
+}
+.fa-map-o:before {
+ content: "\f278";
+}
+.fa-map:before {
+ content: "\f279";
+}
+.fa-commenting:before {
+ content: "\f27a";
+}
+.fa-commenting-o:before {
+ content: "\f27b";
+}
+.fa-houzz:before {
+ content: "\f27c";
+}
+.fa-vimeo:before {
+ content: "\f27d";
+}
+.fa-black-tie:before {
+ content: "\f27e";
+}
+.fa-fonticons:before {
+ content: "\f280";
+}
+.fa-reddit-alien:before {
+ content: "\f281";
+}
+.fa-edge:before {
+ content: "\f282";
+}
+.fa-credit-card-alt:before {
+ content: "\f283";
+}
+.fa-codiepie:before {
+ content: "\f284";
+}
+.fa-modx:before {
+ content: "\f285";
+}
+.fa-fort-awesome:before {
+ content: "\f286";
+}
+.fa-usb:before {
+ content: "\f287";
+}
+.fa-product-hunt:before {
+ content: "\f288";
+}
+.fa-mixcloud:before {
+ content: "\f289";
+}
+.fa-scribd:before {
+ content: "\f28a";
+}
+.fa-pause-circle:before {
+ content: "\f28b";
+}
+.fa-pause-circle-o:before {
+ content: "\f28c";
+}
+.fa-stop-circle:before {
+ content: "\f28d";
+}
+.fa-stop-circle-o:before {
+ content: "\f28e";
+}
+.fa-shopping-bag:before {
+ content: "\f290";
+}
+.fa-shopping-basket:before {
+ content: "\f291";
+}
+.fa-hashtag:before {
+ content: "\f292";
+}
+.fa-bluetooth:before {
+ content: "\f293";
+}
+.fa-bluetooth-b:before {
+ content: "\f294";
+}
+.fa-percent:before {
+ content: "\f295";
+}
+.fa-gitlab:before {
+ content: "\f296";
+}
+.fa-wpbeginner:before {
+ content: "\f297";
+}
+.fa-wpforms:before {
+ content: "\f298";
+}
+.fa-envira:before {
+ content: "\f299";
+}
+.fa-universal-access:before {
+ content: "\f29a";
+}
+.fa-wheelchair-alt:before {
+ content: "\f29b";
+}
+.fa-question-circle-o:before {
+ content: "\f29c";
+}
+.fa-blind:before {
+ content: "\f29d";
+}
+.fa-audio-description:before {
+ content: "\f29e";
+}
+.fa-volume-control-phone:before {
+ content: "\f2a0";
+}
+.fa-braille:before {
+ content: "\f2a1";
+}
+.fa-assistive-listening-systems:before {
+ content: "\f2a2";
+}
+.fa-asl-interpreting:before,
+.fa-american-sign-language-interpreting:before {
+ content: "\f2a3";
+}
+.fa-deafness:before,
+.fa-hard-of-hearing:before,
+.fa-deaf:before {
+ content: "\f2a4";
+}
+.fa-glide:before {
+ content: "\f2a5";
+}
+.fa-glide-g:before {
+ content: "\f2a6";
+}
+.fa-signing:before,
+.fa-sign-language:before {
+ content: "\f2a7";
+}
+.fa-low-vision:before {
+ content: "\f2a8";
+}
+.fa-viadeo:before {
+ content: "\f2a9";
+}
+.fa-viadeo-square:before {
+ content: "\f2aa";
+}
+.fa-snapchat:before {
+ content: "\f2ab";
+}
+.fa-snapchat-ghost:before {
+ content: "\f2ac";
+}
+.fa-snapchat-square:before {
+ content: "\f2ad";
+}
+.fa-pied-piper:before {
+ content: "\f2ae";
+}
+.fa-first-order:before {
+ content: "\f2b0";
+}
+.fa-yoast:before {
+ content: "\f2b1";
+}
+.fa-themeisle:before {
+ content: "\f2b2";
+}
+.fa-google-plus-circle:before,
+.fa-google-plus-official:before {
+ content: "\f2b3";
+}
+.fa-fa:before,
+.fa-font-awesome:before {
+ content: "\f2b4";
+}
+.fa-handshake-o:before {
+ content: "\f2b5";
+}
+.fa-envelope-open:before {
+ content: "\f2b6";
+}
+.fa-envelope-open-o:before {
+ content: "\f2b7";
+}
+.fa-linode:before {
+ content: "\f2b8";
+}
+.fa-address-book:before {
+ content: "\f2b9";
+}
+.fa-address-book-o:before {
+ content: "\f2ba";
+}
+.fa-vcard:before,
+.fa-address-card:before {
+ content: "\f2bb";
+}
+.fa-vcard-o:before,
+.fa-address-card-o:before {
+ content: "\f2bc";
+}
+.fa-user-circle:before {
+ content: "\f2bd";
+}
+.fa-user-circle-o:before {
+ content: "\f2be";
+}
+.fa-user-o:before {
+ content: "\f2c0";
+}
+.fa-id-badge:before {
+ content: "\f2c1";
+}
+.fa-drivers-license:before,
+.fa-id-card:before {
+ content: "\f2c2";
+}
+.fa-drivers-license-o:before,
+.fa-id-card-o:before {
+ content: "\f2c3";
+}
+.fa-quora:before {
+ content: "\f2c4";
+}
+.fa-free-code-camp:before {
+ content: "\f2c5";
+}
+.fa-telegram:before {
+ content: "\f2c6";
+}
+.fa-thermometer-4:before,
+.fa-thermometer:before,
+.fa-thermometer-full:before {
+ content: "\f2c7";
+}
+.fa-thermometer-3:before,
+.fa-thermometer-three-quarters:before {
+ content: "\f2c8";
+}
+.fa-thermometer-2:before,
+.fa-thermometer-half:before {
+ content: "\f2c9";
+}
+.fa-thermometer-1:before,
+.fa-thermometer-quarter:before {
+ content: "\f2ca";
+}
+.fa-thermometer-0:before,
+.fa-thermometer-empty:before {
+ content: "\f2cb";
+}
+.fa-shower:before {
+ content: "\f2cc";
+}
+.fa-bathtub:before,
+.fa-s15:before,
+.fa-bath:before {
+ content: "\f2cd";
+}
+.fa-podcast:before {
+ content: "\f2ce";
+}
+.fa-window-maximize:before {
+ content: "\f2d0";
+}
+.fa-window-minimize:before {
+ content: "\f2d1";
+}
+.fa-window-restore:before {
+ content: "\f2d2";
+}
+.fa-times-rectangle:before,
+.fa-window-close:before {
+ content: "\f2d3";
+}
+.fa-times-rectangle-o:before,
+.fa-window-close-o:before {
+ content: "\f2d4";
+}
+.fa-bandcamp:before {
+ content: "\f2d5";
+}
+.fa-grav:before {
+ content: "\f2d6";
+}
+.fa-etsy:before {
+ content: "\f2d7";
+}
+.fa-imdb:before {
+ content: "\f2d8";
+}
+.fa-ravelry:before {
+ content: "\f2d9";
+}
+.fa-eercast:before {
+ content: "\f2da";
+}
+.fa-microchip:before {
+ content: "\f2db";
+}
+.fa-snowflake-o:before {
+ content: "\f2dc";
+}
+.fa-superpowers:before {
+ content: "\f2dd";
+}
+.fa-wpexplorer:before {
+ content: "\f2de";
+}
+.fa-meetup:before {
+ content: "\f2e0";
+}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+}
+.sr-only-focusable:active,
+.sr-only-focusable:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+}
diff --git a/src/assets/css/font-awesome.min.css b/src/assets/css/font-awesome.min.css
new file mode 100644
index 0000000..540440c
--- /dev/null
+++ b/src/assets/css/font-awesome.min.css
@@ -0,0 +1,4 @@
+/*!
+ * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
diff --git a/src/assets/css/global.scss b/src/assets/css/global.scss
new file mode 100644
index 0000000..f90a11e
--- /dev/null
+++ b/src/assets/css/global.scss
@@ -0,0 +1,3 @@
+.#{$globalPrefixCls}-hidden {
+ display: none !important;
+}
diff --git a/src/assets/css/global_vars.scss b/src/assets/css/global_vars.scss
new file mode 100644
index 0000000..6aa755b
--- /dev/null
+++ b/src/assets/css/global_vars.scss
@@ -0,0 +1,9 @@
+$globalPrefixCls: 'qa';
+
+$blue: #2e61c8;
+
+$info: #2dbe8a;
+
+$warning: #ffb142;
+
+$error: #f43530;
diff --git a/src/assets/fonts/FontAwesome.otf b/src/assets/fonts/FontAwesome.otf
new file mode 100644
index 0000000..401ec0f
Binary files /dev/null and b/src/assets/fonts/FontAwesome.otf differ
diff --git a/src/assets/fonts/fontawesome-webfont.eot b/src/assets/fonts/fontawesome-webfont.eot
new file mode 100644
index 0000000..e9f60ca
Binary files /dev/null and b/src/assets/fonts/fontawesome-webfont.eot differ
diff --git a/src/assets/fonts/fontawesome-webfont.svg b/src/assets/fonts/fontawesome-webfont.svg
new file mode 100644
index 0000000..855c845
--- /dev/null
+++ b/src/assets/fonts/fontawesome-webfont.svg
@@ -0,0 +1,2671 @@
+
+
+
diff --git a/src/assets/fonts/fontawesome-webfont.ttf b/src/assets/fonts/fontawesome-webfont.ttf
new file mode 100644
index 0000000..35acda2
Binary files /dev/null and b/src/assets/fonts/fontawesome-webfont.ttf differ
diff --git a/src/assets/fonts/fontawesome-webfont.woff b/src/assets/fonts/fontawesome-webfont.woff
new file mode 100644
index 0000000..400014a
Binary files /dev/null and b/src/assets/fonts/fontawesome-webfont.woff differ
diff --git a/src/assets/fonts/fontawesome-webfont.woff2 b/src/assets/fonts/fontawesome-webfont.woff2
new file mode 100644
index 0000000..4d13fc6
Binary files /dev/null and b/src/assets/fonts/fontawesome-webfont.woff2 differ
diff --git a/src/assets/ruiwen.jpeg b/src/assets/images/favicon.jpg
similarity index 100%
rename from src/assets/ruiwen.jpeg
rename to src/assets/images/favicon.jpg
diff --git a/src/assets/images/ruiwen.gif b/src/assets/images/ruiwen.gif
new file mode 100755
index 0000000..e5e9ad8
Binary files /dev/null and b/src/assets/images/ruiwen.gif differ
diff --git a/src/assets/images/success.svg b/src/assets/images/success.svg
new file mode 100755
index 0000000..1e8a895
--- /dev/null
+++ b/src/assets/images/success.svg
@@ -0,0 +1,18 @@
+
diff --git a/src/assets/index.d.ts b/src/assets/index.d.ts
new file mode 100644
index 0000000..658d1ac
--- /dev/null
+++ b/src/assets/index.d.ts
@@ -0,0 +1,5 @@
+declare module '*.svg'
+declare module '*.png'
+declare module '*.jpg'
+declare module '*.jpeg'
+declare module '*.gif'
diff --git a/src/common/index.ts b/src/common/index.ts
new file mode 100644
index 0000000..82483f6
--- /dev/null
+++ b/src/common/index.ts
@@ -0,0 +1,7 @@
+const PREFIXCLS = 'rl'
+
+const DELAY_TIME = 2.5
+
+const API_URL = 'http://omyleon.com:6260/v1'
+
+export { PREFIXCLS, DELAY_TIME, API_URL }
diff --git a/src/components/Hello/styles/index.less b/src/components/ExampleCom/index.less
similarity index 67%
rename from src/components/Hello/styles/index.less
rename to src/components/ExampleCom/index.less
index efb7aa5..ab2c128 100644
--- a/src/components/Hello/styles/index.less
+++ b/src/components/ExampleCom/index.less
@@ -1,10 +1,12 @@
-@import "./theme";
+@color: #5ab4e3;
+
.h2 {
color: blue;
}
.name {
color: @color;
+ margin-top: 10px;
}
.test {
@@ -15,8 +17,9 @@
.ruiwen {
width: 300px;
height: 300px;
+ margin-top: 20px;
background-position: center;
transform: translateX(100px);
background-size: 300px 300px;
- background-image: url(../../../assets/ruiwen.jpeg)
+ background-image: url(../../assets/images/ruiwen.gif)
}
diff --git a/src/components/ExampleCom/index.tsx b/src/components/ExampleCom/index.tsx
new file mode 100644
index 0000000..5287b7c
--- /dev/null
+++ b/src/components/ExampleCom/index.tsx
@@ -0,0 +1,32 @@
+import * as React from 'react'
+
+import './index.less'
+import { Button } from 'antd-mobile'
+
+interface IProps {
+ name: string
+}
+
+class Hello extends React.PureComponent {
+ state = { count: 1 }
+
+ handleClick = () => {
+ const { count } = this.state
+ this.setState({ count: count + 1 })
+ }
+
+ render() {
+ const { name } = this.props
+ const { count } = this.state
+ return (
+
+
This is Example component.
+
{`my name is ${name}`}
+
+
this is ruiwen
+
+ )
+ }
+}
+
+export default Hello
diff --git a/src/components/Hello/index.js b/src/components/Hello/index.js
deleted file mode 100644
index 86dac61..0000000
--- a/src/components/Hello/index.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-
-import './styles'
-
-class Hello extends PureComponent {
- static propTypes = {
- name: PropTypes.string.isRequired,
- }
-
- state = {
- count: 1,
- }
-
- handleClick = () => {
- const { count } = this.state
- this.setState({ count: count + 1 })
- }
-
- render() {
- const { name } = this.props
- const { count } = this.state
- return (
-
-
This is from Hello component.
-
this is h2
-
- my name is
- {name}
-
-
-
this is ruiwen
-
- )
- }
-}
-
-export default Hello
diff --git a/src/components/Hello/loadable.js b/src/components/Hello/loadable.js
deleted file mode 100644
index 9aefe10..0000000
--- a/src/components/Hello/loadable.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react'
-import Loadable from 'react-loadable'
-
-export default Loadable({
- loader: () => import('./index'),
- loading: () => loading...
,
-})
diff --git a/src/components/Hello/styles/theme.less b/src/components/Hello/styles/theme.less
deleted file mode 100644
index eb30f1c..0000000
--- a/src/components/Hello/styles/theme.less
+++ /dev/null
@@ -1 +0,0 @@
-@color: #4D926F;
diff --git a/src/docs/index.md b/src/docs/index.md
deleted file mode 100644
index 9695975..0000000
--- a/src/docs/index.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# This is a markdown file in docs file
-
-这是不需要处理,直接拷贝的文件,如果你没有,可以在 config/options 中进行设置
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index 5610cca..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react'
-import { render } from 'react-dom'
-import AppHot from './App.hot'
-
-const root = document.getElementById('root')
-
-function renderAppToContainer() {
- render(, root)
-}
-
-renderAppToContainer()
diff --git a/src/index.less b/src/index.less
deleted file mode 100644
index 79dbe6b..0000000
--- a/src/index.less
+++ /dev/null
@@ -1,21 +0,0 @@
-body {
- margin: 0;
- padding: 0 30px;
-}
-
-h1 {
- color: red;
-}
-
-.div {
- font-size: 45px;
-}
-
-li { /* 不用的样式,打包会去掉 */
- list-style: none;
-}
-
-.btn {
- height: 60px;
- line-height: 60px;
-}
diff --git a/src/index.tmpl.html b/src/index.tmpl.html
index 655c174..2fc17af 100644
--- a/src/index.tmpl.html
+++ b/src/index.tmpl.html
@@ -1,13 +1,38 @@
-
-
-
- <%= htmlWebpackPlugin.options.title %>
-
-
-
-
-
-
+
+
+
+ <%= htmlWebpackPlugin.options.title %>
+
+
+
+
+
+
+
+
+
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..eb9fb21
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,7 @@
+import * as React from 'react'
+import { render } from 'react-dom'
+import AppHot from './App'
+
+const root = document.getElementById('root')
+
+render(, root)
diff --git a/src/interface.ts b/src/interface.ts
new file mode 100644
index 0000000..172275e
--- /dev/null
+++ b/src/interface.ts
@@ -0,0 +1,16 @@
+interface IReqOptions {
+ uri?: string
+ query?: object | null
+ data?: { [key: string]: any }
+}
+
+type resType = 'success' | 'fail' | 'info'
+
+interface IResponse {
+ status: number
+ statusText: string
+ data: { type: resType; message: string; [key: string]: any }
+ lists?: any[]
+}
+
+export { IReqOptions, resType, IResponse }
diff --git a/src/mobx/action.js b/src/mobx/action.js
new file mode 100644
index 0000000..97dec0f
--- /dev/null
+++ b/src/mobx/action.js
@@ -0,0 +1,16 @@
+import { actionNameSymbol, appActionKeySymbol, actionTypeSymbol } from './meta';
+import { App } from './app';
+
+const mAction = (config = {}) => target => {
+ target[actionNameSymbol] = config.name || target.name || null;
+ target[actionTypeSymbol] = config.type;
+ App[appActionKeySymbol] = App[appActionKeySymbol] || [];
+ App[appActionKeySymbol].push({
+ target,
+ name: config.name,
+ page: config.page,
+ });
+ return target;
+};
+
+export { mAction };
diff --git a/src/mobx/app.js b/src/mobx/app.js
new file mode 100644
index 0000000..1756245
--- /dev/null
+++ b/src/mobx/app.js
@@ -0,0 +1,194 @@
+import invariant from 'invariant';
+import {
+ instanceType,
+ storeTypeSymbol,
+ storeInstanceSymbol,
+ getInstanceFuncSymbol,
+ singleInstanceSymbol,
+ appActionKeySymbol,
+ appStoreKeySymbol,
+} from './meta';
+import { defineReadOnlyProperty, firstToLowercase } from './utils';
+import AsyncManager from './zone';
+
+export class App {
+ rootStore = {};
+ rootAction = {};
+
+ constructor() {
+ this.asyncManager = AsyncManager.getInstance();
+ this.processStores();
+ this.processActions();
+ }
+
+ processStores() {
+ let scannedStores = (App[appStoreKeySymbol] || []).reduce((ret, item) => {
+ ret[item.page] = ret[item.page] || {};
+ ret[item.page][item.name] = item.target;
+ return ret;
+ }, {});
+
+ //扫描的文件
+ Object.keys(scannedStores).forEach(pageKey => {
+ invariant(
+ !this.rootStore[pageKey],
+ `dumplicated page or component name for ${pageKey}`
+ );
+
+ defineReadOnlyProperty(this.rootStore, pageKey, {});
+
+ let pageStore = scannedStores[pageKey];
+ this.processPageStore(pageStore, this.rootStore[pageKey]);
+ });
+
+ }
+
+ processPageStore(pageStore, finalPageStore) {
+ Object.keys(pageStore).forEach(storeKey => {
+ this.processSingleStore(pageStore[storeKey], storeKey, finalPageStore);
+ });
+ }
+
+ processSingleStore(storeClassOrInstance, storeKey, finalPageStore) {
+ let asyncManager = this.asyncManager;
+ if (typeof storeClassOrInstance == 'function') {
+ if (
+ storeClassOrInstance[storeTypeSymbol] === instanceType.singleton ||
+ storeClassOrInstance[storeTypeSymbol] === undefined
+ ) {
+ Object.defineProperty(finalPageStore, firstToLowercase(storeKey), {
+ configurable: true,
+ enumerable: true,
+ get() {
+ storeClassOrInstance[singleInstanceSymbol] =
+ storeClassOrInstance[singleInstanceSymbol] ||
+ new storeClassOrInstance();
+
+ asyncManager.wrapStore(storeClassOrInstance[singleInstanceSymbol]);
+
+ return storeClassOrInstance[singleInstanceSymbol];
+ },
+ set() {
+ throw Error('can not set store again');
+ },
+ });
+ } else {
+ this.processMultiInstance(storeClassOrInstance);
+
+ Object.defineProperty(finalPageStore, firstToLowercase(storeKey), {
+ configurable: true,
+ enumerable: true,
+ get() {
+ return storeClassOrInstance[getInstanceFuncSymbol];
+ },
+ set() {
+ throw Error('can not set store again');
+ },
+ });
+ }
+ } else {
+ asyncManager.wrapStore(storeClassOrInstance);
+ defineReadOnlyProperty(
+ finalPageStore,
+ firstToLowercase(storeKey),
+ storeClassOrInstance
+ );
+ }
+ }
+
+ processMultiInstance(storeClassOrInstance) {
+ let asyncManager = this.asyncManager;
+
+ storeClassOrInstance[storeInstanceSymbol] =
+ storeClassOrInstance[storeInstanceSymbol] || {};
+
+ storeClassOrInstance[getInstanceFuncSymbol] =
+ storeClassOrInstance[getInstanceFuncSymbol] ||
+ function(uniqueKey) {
+ storeClassOrInstance[storeInstanceSymbol][uniqueKey] =
+ storeClassOrInstance[storeInstanceSymbol][uniqueKey] ||
+ new storeClassOrInstance();
+
+ asyncManager.wrapStore(
+ storeClassOrInstance[storeInstanceSymbol][uniqueKey]
+ );
+
+ return storeClassOrInstance[storeInstanceSymbol][uniqueKey];
+ };
+ }
+
+ processActions() {
+ let scannedActions = (App[appActionKeySymbol] || []).reduce((ret, item) => {
+ ret[item.page] = ret[item.page] || {};
+ ret[item.page][item.name] = item.target;
+ return ret;
+ }, {});
+
+ Object.keys(scannedActions).forEach(pageKey => {
+ defineReadOnlyProperty(this.rootAction, pageKey, {});
+
+ let pageAction = scannedActions[pageKey];
+ let pageStore = this.rootStore[pageKey];
+ this.processPageAction(pageAction, this.rootAction[pageKey], pageStore);
+ });
+ }
+
+ processPageAction(pageAction, finalPageAction, pageStore) {
+ Object.keys(pageAction).forEach(actionKey => {
+ this.processSingleAction(
+ pageAction[actionKey],
+ actionKey,
+ finalPageAction,
+ pageStore
+ );
+ });
+ }
+
+ processSingleAction(
+ actionClassOrInstance,
+ actionKey,
+ finalPageAction,
+ pageStore
+ ) {
+ let asyncManager = this.asyncManager;
+
+ if (typeof actionClassOrInstance == 'function') {
+ Object.defineProperty(finalPageAction, firstToLowercase(actionKey), {
+ configurable: true,
+ enumerable: true,
+ get() {
+ actionClassOrInstance[singleInstanceSymbol] =
+ actionClassOrInstance[singleInstanceSymbol] ||
+ new actionClassOrInstance(
+ pageStore,
+ finalPageAction,
+ this.rootStore,
+ this.rootAction
+ );
+
+ asyncManager.wrapAction(actionClassOrInstance[singleInstanceSymbol]);
+
+ return actionClassOrInstance[singleInstanceSymbol];
+ },
+ set() {
+ throw Error('can not set action again');
+ },
+ });
+ } else {
+ asyncManager.wrapAction(actionClassOrInstance);
+
+ defineReadOnlyProperty(
+ finalPageAction,
+ firstToLowercase(actionKey),
+ actionClassOrInstance
+ );
+ }
+ }
+}
+
+let app = null;
+
+export function createApp() {
+ app = app || new App();
+ return app;
+}
diff --git a/src/mobx/index.js b/src/mobx/index.js
new file mode 100644
index 0000000..9158559
--- /dev/null
+++ b/src/mobx/index.js
@@ -0,0 +1,4 @@
+export { mStore } from './store';
+export { mAction } from './action';
+export { instanceType } from './meta';
+export { provider } from './provider'; // 放最后,顺序不能乱,否则有循环依赖问题
diff --git a/src/mobx/meta.js b/src/mobx/meta.js
new file mode 100644
index 0000000..690bd71
--- /dev/null
+++ b/src/mobx/meta.js
@@ -0,0 +1,22 @@
+export const storeNameSymbol = getSymbol('store-name');
+export const storeTypeSymbol = getSymbol('store-type');
+export const storeInstanceSymbol = getSymbol('store-instance'); //多实例缓存
+export const singleInstanceSymbol = getSymbol('single-instance'); //单实例缓存
+export const getInstanceFuncSymbol = getSymbol('get-instance-func'); //多实例根据key获取实例
+export const actionNameSymbol = getSymbol('action-name');
+export const actionTypeSymbol = getSymbol('action-type');
+export const appStoreKeySymbol = getSymbol('app-store-key');
+export const appActionKeySymbol = getSymbol('app-action-key');
+
+export const asyncManagerSymbol = getSymbol('async-manager');
+export const storeWrappedSymbol = getSymbol('store-has-wrapped');
+export const actionWrappedSymbol = getSymbol('action-has-wrapped');
+
+export const instanceType = {
+ singleton: getSymbol('singleton'),
+ multi: getSymbol('multi'),
+};
+
+function getSymbol(name) {
+ return typeof Symbol !== 'undefined' ? Symbol(name) : `__symbol-${name}__`;
+}
diff --git a/src/mobx/provider.js b/src/mobx/provider.js
new file mode 100644
index 0000000..e60a3d9
--- /dev/null
+++ b/src/mobx/provider.js
@@ -0,0 +1,57 @@
+import React, { Component } from 'react';
+import { Provider } from 'mobx-react';
+import { createApp } from './app';
+
+const provider = WrapComponentOrElement => {
+ let app = createApp();
+
+ if (React.isValidElement(WrapComponentOrElement)) {
+ let componentName = getDisplayName(WrapComponentOrElement.type);
+
+ return (
+
+ {WrapComponentOrElement}
+
+ );
+ } else {
+ return class WrapperComponent extends Component {
+ static displayName = `Provider(${getDisplayName(
+ WrapComponentOrElement
+ )})`;
+
+ constructor(props) {
+ super(props);
+ }
+
+ componentWillReceiveProps(nextProps) {}
+
+ render() {
+ const props = this.props;
+ return (
+
+
+ {this.props.children}
+
+
+ );
+ }
+ };
+ }
+};
+
+function getDisplayName(WrappedComponent) {
+ return WrappedComponent.displayName || WrappedComponent.name || 'Component';
+}
+
+export {
+ provider,
+ getDisplayName,
+}
diff --git a/src/mobx/store.js b/src/mobx/store.js
new file mode 100644
index 0000000..6040572
--- /dev/null
+++ b/src/mobx/store.js
@@ -0,0 +1,17 @@
+import {
+ storeNameSymbol,
+ appStoreKeySymbol,
+ instanceType,
+ storeTypeSymbol,
+} from './meta';
+import { App } from './app';
+
+const mStore = (config = { type: instanceType.singleton }) => target => {
+ target[storeNameSymbol] = config.name || target.name || null;
+ target[storeTypeSymbol] = config.type;
+ App[appStoreKeySymbol] = App[appStoreKeySymbol] || [];
+ App[appStoreKeySymbol].push({ target, name: config.name, page: config.page });
+ return target;
+};
+
+export { mStore };
diff --git a/src/mobx/utils.js b/src/mobx/utils.js
new file mode 100644
index 0000000..4da48f7
--- /dev/null
+++ b/src/mobx/utils.js
@@ -0,0 +1,42 @@
+export const defineReadOnlyProperty = (
+ target,
+ name,
+ value,
+ message = 'property is readonly'
+) => {
+ Object.defineProperty(target, name, {
+ configurable: true,
+ enumerable: true,
+ get() {
+ return value;
+ },
+ set() {
+ throw Error(message);
+ },
+ });
+};
+
+export const defineHiddenProperty = (target, name, value) => {
+ Object.defineProperty(target, name, {
+ configurable: true,
+ enumerable: false,
+ writable: false,
+ value,
+ });
+};
+
+export function firstToLowercase(str) {
+ return str.charAt(0).toLowerCase() + str.substr(1);
+}
+
+export function isPromiseLike(p) {
+ if (
+ p &&
+ typeof p === 'object' &&
+ typeof p.then === 'function' &&
+ typeof p.catch === 'function'
+ ) {
+ return true;
+ }
+ return false;
+}
diff --git a/src/mobx/zone.js b/src/mobx/zone.js
new file mode 100644
index 0000000..d8ee33e
--- /dev/null
+++ b/src/mobx/zone.js
@@ -0,0 +1,110 @@
+import 'zone.js';
+import {
+ asyncManagerSymbol,
+ storeWrappedSymbol,
+ actionWrappedSymbol,
+} from './meta';
+import { defineHiddenProperty } from './utils';
+
+export default class AsyncManager {
+ static singleton = null;
+
+ static getInstance() {
+ if (!this.singleton) {
+ new this();
+ }
+
+ return this.singleton;
+ }
+
+ zone;
+
+ constructor() {
+ if (AsyncManager.singleton !== null) return AsyncManager.singleton;
+
+ this.init();
+ AsyncManager.singleton = this;
+ }
+
+ init() {
+ this.zone = Zone.root.fork({
+ name: asyncManagerSymbol,
+ });
+ }
+
+ wrapStore(storeInstance) {
+ if (process.env.NODE_ENV !== 'production') {
+ if (this.storeHasWrapped(storeInstance)) {
+ return;
+ }
+
+ let propKeys = this.getFunctionKeys(storeInstance);
+ propKeys.forEach(prop => {
+ if (typeof storeInstance[prop] === 'function') {
+ storeInstance[prop] = this.wrapStoreFunc(
+ storeInstance[prop],
+ storeInstance,
+ prop
+ );
+ }
+ });
+ defineHiddenProperty(storeInstance, storeWrappedSymbol, true);
+ }
+ }
+
+ wrapStoreFunc = (fn, context, prop) => {
+ return (...args) => {
+ if (Zone.current !== this.zone) {
+ console.error(
+ `store ${context.constructor &&
+ context.constructor.name} 的方法${prop}必须在action里调用`
+ );
+ return;
+ }
+ return fn.apply(context, args);
+ };
+ };
+
+ storeHasWrapped(storeInstance) {
+ return storeInstance && storeInstance[storeWrappedSymbol] === true;
+ }
+
+ actionHasWrapped(actionInstance) {
+ return actionInstance && actionInstance[actionWrappedSymbol] === true;
+ }
+
+ wrapAction(actionInstance) {
+ if (process.env.NODE_ENV !== 'production') {
+ if (this.actionHasWrapped(actionInstance)) {
+ return;
+ }
+ let propKeys = this.getFunctionKeys(actionInstance);
+ propKeys.forEach(prop => {
+ if (typeof actionInstance[prop] === 'function') {
+ actionInstance[prop] = this.wrapActionFunc(actionInstance, prop);
+ }
+ });
+
+ defineHiddenProperty(actionInstance, actionWrappedSymbol, true);
+ }
+ }
+
+ wrapActionFunc(action, prop) {
+ let origin = action[prop];
+ return (...args) => {
+ return this.zone.run(origin, action, args);
+ };
+ }
+
+ getFunctionKeys(obj) {
+ //获取实例和原型上的属性
+ let objProto = Object.getPrototypeOf(obj) || obj.__proto__;
+ let propKeys = [
+ ...Object.keys(obj),
+ ...Object.getOwnPropertyNames(objProto),
+ ];
+ propKeys = Array.from(new Set(propKeys));
+ propKeys = propKeys.filter(key => key !== 'constructor');
+ return propKeys;
+ }
+}
diff --git a/src/mobxDependence.ts b/src/mobxDependence.ts
new file mode 100644
index 0000000..7863fa0
--- /dev/null
+++ b/src/mobxDependence.ts
@@ -0,0 +1,2 @@
+import './pages/Example/stores/exampleStore'
+import './pages/Example/actions/exampleAction'
diff --git a/src/pages/404/index.scss b/src/pages/404/index.scss
new file mode 100644
index 0000000..8557cba
--- /dev/null
+++ b/src/pages/404/index.scss
@@ -0,0 +1,12 @@
+$prefixCls: 'page-404';
+
+.#{$prefixCls} {
+ height: 100vh;
+
+ h1 {
+ padding-top: 500px;
+ text-align: center;
+ margin: 0;
+ font-size: 45px;
+ }
+}
diff --git a/src/pages/404/index.tsx b/src/pages/404/index.tsx
new file mode 100644
index 0000000..041d5e4
--- /dev/null
+++ b/src/pages/404/index.tsx
@@ -0,0 +1,11 @@
+import * as React from 'react'
+
+import './index.scss'
+
+const NoMatch = () => (
+
+
404! page not found...
+
+)
+
+export default NoMatch
diff --git a/src/pages/Example/Loadable.tsx b/src/pages/Example/Loadable.tsx
new file mode 100644
index 0000000..8098810
--- /dev/null
+++ b/src/pages/Example/Loadable.tsx
@@ -0,0 +1,23 @@
+import * as React from 'react'
+import Loadable, { LoadingComponentProps } from 'react-loadable'
+
+class LoadingComponent extends React.PureComponent {
+ render() {
+ const { error, isLoading, pastDelay, timedOut } = this.props
+ return (
+
+ {error}
+ {isLoading}
+ {pastDelay}
+ {timedOut}
+
+ )
+ }
+}
+
+export default Loadable({
+ loader: () => import('./index'),
+ loading: LoadingComponent,
+ delay: 100,
+ timeout: 10000,
+})
diff --git a/src/pages/Example/actions/exampleAction.ts b/src/pages/Example/actions/exampleAction.ts
new file mode 100644
index 0000000..09f5e2e
--- /dev/null
+++ b/src/pages/Example/actions/exampleAction.ts
@@ -0,0 +1,17 @@
+import { IRootAction, IRootStore } from 'typings'
+import { mAction } from '../../../mobx/action'
+import { getGoods } from '../apis'
+
+@mAction
+export default class ExampleAction {
+ constructor(public stores: IRootStore['Example'], public actions: IRootAction['Example']) {}
+
+ async loadGoods() {
+ const { exampleStore } = this.stores
+
+ exampleStore
+ .setLoading(true)
+ .setCurGoods(await getGoods())
+ .setLoading(false)
+ }
+}
diff --git a/src/pages/Example/apis.ts b/src/pages/Example/apis.ts
new file mode 100644
index 0000000..ab8d10d
--- /dev/null
+++ b/src/pages/Example/apis.ts
@@ -0,0 +1,13 @@
+export async function getGoods() {
+ await wait()
+ const random = Math.random().toFixed(3)
+ return {
+ id: random,
+ name: `goods-${random}`,
+ desc: `desc-${random}`,
+ }
+}
+
+export function wait(delay = 400) {
+ return new Promise(r => setTimeout(r, delay))
+}
diff --git a/src/pages/Example/index.scss b/src/pages/Example/index.scss
new file mode 100644
index 0000000..ca70172
--- /dev/null
+++ b/src/pages/Example/index.scss
@@ -0,0 +1,37 @@
+$prefixCls: 'page-example';
+
+.#{$prefixCls} {
+ h1, h2 {
+ text-align: center;
+ }
+
+ ul {
+ padding: 0;
+ background: $info;
+
+ li {
+ list-style: none;
+ height: 60px;
+ line-height: 60px;
+ text-align: center;
+ font-size: 30px;
+ }
+ }
+
+ .am-button {
+ width: 60%;
+ height: 60px;
+ line-height: 60px;
+ margin: 0 auto;
+
+ .icon-right {
+ margin-left: 10px;
+ vertical-align: -5px;
+ }
+ }
+}
+
+.loading {
+ text-align: center;
+ margin-top: 50px;
+}
diff --git a/src/pages/Example/index.tsx b/src/pages/Example/index.tsx
new file mode 100644
index 0000000..a24b565
--- /dev/null
+++ b/src/pages/Example/index.tsx
@@ -0,0 +1,69 @@
+import * as React from 'react'
+import { inject, observer } from 'mobx-react'
+import { Button } from 'antd-mobile'
+import ExampleCom from 'components/ExampleCom'
+import { IRootStore, IRootAction } from 'typings'
+
+import './index.scss'
+
+import successIcon from 'assets/images/success.svg'
+
+@inject(injector)
+@observer
+export default class Example extends React.Component {
+ static defaultProps = {
+ prefixCls: 'page-example',
+ }
+
+ componentDidMount() {
+ this.handleLoadGoods()
+ }
+
+ handleLoadGoods = () => {
+ const { action } = this.props
+
+ action!.loadGoods()
+ }
+
+ render() {
+ const { prefixCls, store } = this.props
+ const {
+ curGoods: { name, desc },
+ loading,
+ } = store!
+
+ return (
+
+
this is example page
+
+
![successIcon]({successIcon})
+
当前产品
+ {loading ? (
+
loading...
+ ) : (
+
+ )}
+
+
+ )
+ }
+}
+
+type injectorReturnType = ReturnType
+
+interface IProps extends Partial {
+ prefixCls?: string
+}
+
+function injector({ rootStore, rootAction }: { rootStore: IRootStore; rootAction: IRootAction }) {
+ return {
+ store: rootStore.Example.exampleStore,
+ action: rootAction.Example.exampleAction,
+ }
+}
diff --git a/src/pages/Example/stores/exampleStore.ts b/src/pages/Example/stores/exampleStore.ts
new file mode 100644
index 0000000..0d7f815
--- /dev/null
+++ b/src/pages/Example/stores/exampleStore.ts
@@ -0,0 +1,31 @@
+import { action, observable, computed } from 'mobx'
+import { mStore } from '../../../mobx/store'
+
+interface IGoods {
+ id: string
+ name: string
+ desc: string
+}
+
+@mStore
+export default class ExampleStore {
+ @observable
+ curGoods = {} as IGoods
+
+ @observable
+ loading = true
+
+ @action
+ setCurGoods(goods: IGoods) {
+ this.curGoods = goods
+
+ return this
+ }
+
+ @action
+ setLoading(flag: boolean) {
+ this.loading = flag
+
+ return this
+ }
+}
diff --git a/src/routes/config.ts b/src/routes/config.ts
new file mode 100644
index 0000000..3b1eb86
--- /dev/null
+++ b/src/routes/config.ts
@@ -0,0 +1,21 @@
+import * as React from 'react'
+import ExamplePage from 'pages/Example/Loadable'
+import NoMatchPage from 'pages/404'
+
+export interface IRoute {
+ key: string
+ path?: string
+ component?: React.ComponentClass | React.FunctionComponent
+ props?: object
+ exact?: boolean
+ redirect?: string
+ children?: IRoute[]
+}
+
+const config: IRoute[] = [
+ { key: 'home', path: '/', exact: true, redirect: '/example' },
+ { key: 'example', path: '/example', component: ExamplePage },
+ { key: '404', component: NoMatchPage },
+]
+
+export default config
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
new file mode 100644
index 0000000..379077c
--- /dev/null
+++ b/src/routes/index.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react'
+import { Route, Redirect, Switch } from 'react-router-dom'
+import routesConfig from './config'
+
+function getRoutes(routes = routesConfig) {
+ return routes.map(route => {
+ const { key, redirect, children, component: Cmp } = route
+
+ if (children) {
+ return (
+
+ {getRoutes(children)}
+
+ )
+ }
+
+ return redirect ? :
+ })
+}
+
+export default {getRoutes()}
diff --git a/src/tools/add-page.js b/src/tools/add-page.js
new file mode 100644
index 0000000..27bdae7
--- /dev/null
+++ b/src/tools/add-page.js
@@ -0,0 +1,193 @@
+// /
+
+const program = require('commander')
+const FS = require('fs-extra')
+const Path = require('path')
+const ChildProcess = require('child_process')
+const Chalk = require('chalk')
+
+const packageJson = FS.readJSONSync(Path.join(process.cwd(), 'package.json'), {
+ encoding: 'utf-8',
+})
+let { basePath } = packageJson
+
+if (!basePath) {
+ console.log(
+ `you should set the key ${Chalk.red('basePath')} in your ${Chalk.green(
+ 'package.json'
+ )} file. For example, ${Chalk.red('"basePath":"./src"')}`
+ )
+ return
+}
+
+program
+ .version('0.0.2')
+ .option('[] ', 'the path of page or component')
+ .option('-m, --mobx', 'create store and action')
+ .option(
+ '-c, --component',
+ `will create files to ${Chalk.greenBright(
+ Path.join(basePath, 'components')
+ )} directory`
+ )
+
+program.on('--help', () => {
+ console.log(
+ ' This is a quick tool to create a page.\n It can create index.tsx, index.scss file and add <-m> or <--mobx> param can create stores and actions.\n'
+ )
+ console.log('')
+
+ console.log(' Examples:')
+
+ console.log('')
+
+ console.log(
+ ` $ node tools/add-page.js offerList${Chalk.grey(
+ ' // create a page'
+ )}`
+ )
+
+ console.log(
+ ` $ node tools/add-page.js offerList -c${Chalk.grey(
+ ' // create a component for common'
+ )}`
+ )
+
+ console.log(
+ ` $ node tools/add-page.js offerList/tableList${Chalk.grey(
+ ' // create a component inner page offerList'
+ )}`
+ )
+
+ console.log(
+ ` $ node tools/add-page.js offerList -m${Chalk.grey(
+ ' // create a page with stores and actions'
+ )}`
+ )
+
+ console.log('')
+})
+
+program.parse(process.argv)
+
+if (program.args.length < 1) throw new Error('missing param')
+
+const pageOrComPath = program.args[0]
+
+// 跨页面级通用组件独自一个目录
+if (program.component) {
+ basePath = Path.join(basePath, 'components')
+} else {
+ basePath = Path.join(basePath, 'pages')
+}
+
+let componentTpl = FS.readFileSync(
+ `${__dirname}/templates/index.tsx.tpl`,
+ 'utf-8'
+)
+let scssTpl = FS.readFileSync(`${__dirname}/templates/index.scss.tpl`, 'utf-8')
+
+const splitPath = pageOrComPath.split('/').filter(Boolean)
+const rootPageName = splitPath[0] // 页面目录
+const fileDirectoryName = splitPath[splitPath.length - 1] // 页面内最深层目录
+
+const pagePath = Path.join(process.cwd(), basePath, pageOrComPath)
+const rootPagePath = Path.join(process.cwd(), basePath, rootPageName)
+
+const pageTplData = {
+ rootPageName,
+ uppercaseName: fisrtToUppercase(fileDirectoryName),
+ lowercaseName: fisrtToLowercase(fileDirectoryName),
+ splitDashName: toSplitDash(pageOrComPath),
+ relDir: new Array(splitPath.length + 2).join('../'),
+ type: program.component ? 'component' : 'page',
+}
+
+componentTpl = replaceVarible(componentTpl)
+
+scssTpl = replaceVarible(scssTpl)
+
+createFile(`${pagePath}/index.tsx`, componentTpl)
+createFile(`${pagePath}/index.scss`, scssTpl)
+
+if (program.mobx) {
+ let storeTpl = FS.readFileSync(
+ `${__dirname}/templates/stores/store.ts.tpl`,
+ 'utf-8'
+ )
+ let actionTpl = FS.readFileSync(
+ `${__dirname}/templates/actions/action.ts.tpl`,
+ 'utf-8'
+ )
+
+ storeTpl = replaceVarible(storeTpl)
+ actionTpl = replaceVarible(actionTpl)
+
+ const storeName = `${rootPagePath}/stores/${fisrtToLowercase(
+ fileDirectoryName
+ )}Store.ts`
+ const actionName = `${rootPagePath}/actions/${fisrtToLowercase(
+ fileDirectoryName
+ )}Action.ts`
+ createFile(storeName, storeTpl)
+ createFile(actionName, actionTpl)
+
+ ChildProcess.execFile(
+ 'node',
+ [Path.join(__dirname, './sync-mobx.js')],
+ (error, stdout, stderr) => {
+ if (error) {
+ console.error('stderr', stderr)
+ throw error
+ }
+ console.log(Chalk.cyan(stdout))
+ }
+ )
+}
+
+/**
+ * 创建文件
+ * @param path {string} 路径
+ * @defaultFileContent ?{string} 默认文件内容
+ * @return {bollean}
+ */
+function createFile(path, defaultFileContent) {
+ defaultFileContent = defaultFileContent || ''
+ if (FS.existsSync(path)) {
+ console.log(Chalk.red('file already exists: ') + path)
+ return false
+ }
+ FS.outputFileSync(path, defaultFileContent)
+ console.log(`created "${path}".`)
+ return true
+}
+
+// 替换模板变量
+function replaceVarible(tpl) {
+ return tpl.replace(/\$\{(\w+?)\}\$/g, (m, letiable) => pageTplData[letiable])
+}
+
+function toSplitDash(str) {
+ const upperCaseRegex = /[A-Z]+(?=[A-Z][a-z]|$)|[A-Z]/g
+ str = str
+ .split('/')
+ .filter(Boolean)
+ .join('/')
+ // str = str.replace(/\//g, (m, index) => (index ? '_' : ''))
+ str = str.replace(/\//g, '')
+ return str.replace(
+ upperCaseRegex,
+ (m, index) => (index ? '-' : '') + m.toLowerCase()
+ )
+
+ // const lastIdx = str.lastIndexOf('-')
+ // return lastIdx === -1 ? str : str.slice(0, lastIdx)
+}
+
+function fisrtToLowercase(str) {
+ return str.charAt(0).toLowerCase() + str.slice(1)
+}
+
+function fisrtToUppercase(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1)
+}
diff --git a/src/tools/rm-page.js b/src/tools/rm-page.js
new file mode 100644
index 0000000..b03bb8e
--- /dev/null
+++ b/src/tools/rm-page.js
@@ -0,0 +1,124 @@
+// /
+
+const program = require('commander')
+const FS = require('fs-extra')
+const Path = require('path')
+const ChildProcess = require('child_process')
+const Chalk = require('chalk')
+
+const packageJson = FS.readJSONSync(Path.join(process.cwd(), 'package.json'), {
+ encoding: 'utf-8',
+})
+let { basePath } = packageJson
+
+if (!basePath) {
+ console.log(
+ `you should set the key ${Chalk.red('basePath')} in your ${Chalk.green(
+ 'package.json'
+ )} file. For example, ${Chalk.red('"basePath":"./src"')}`
+ )
+ return
+}
+
+program
+ .version('0.0.1')
+ .option('[] ', 'the path of page or component to remove')
+ .option(
+ '-c, --component',
+ `will remove files from ${Chalk.greenBright(
+ Path.join(basePath, 'components')
+ )} directory`
+ )
+
+program.on('--help', () => {
+ console.log(
+ ' This is a quick tool to remove a page or component. \n It will remove index.tsx,index.scss file and stores and actions \n'
+ )
+ console.log('')
+
+ console.log(' Examples:')
+
+ console.log('')
+
+ console.log(
+ ` $ node tools/rm-page.js offerList ${Chalk.grey(
+ '// remove a page'
+ )}`
+ )
+
+ console.log(
+ ` $ node tools/rm-page.js offerList -c ${Chalk.grey(
+ '// remove a component for common'
+ )}`
+ )
+
+ console.log(
+ ` $ node tools/rm-page.js offerList/tableList ${Chalk.grey(
+ '// remove a component inner page offerList'
+ )}`
+ )
+
+ console.log('')
+})
+
+program.parse(process.argv)
+
+if (program.args.length < 1) throw new Error('missing param')
+
+const pageOrComPath = program.args[0]
+
+// 跨页面级通用组件独自一个目录
+if (program.component) {
+ basePath = Path.join(basePath, 'components')
+} else {
+ basePath = Path.join(basePath, 'pages')
+}
+
+const splitPath = pageOrComPath.split('/').filter(Boolean)
+const rootPageName = splitPath[0] // 页面目录
+const fileDirectoryName = splitPath[splitPath.length - 1] // 页面内最深层目录
+
+const pagePath = Path.join(process.cwd(), basePath, pageOrComPath)
+const rootPagePath = Path.join(process.cwd(), basePath, rootPageName)
+
+const storeName = `${rootPagePath}/stores/${fisrtToLowercase(
+ fileDirectoryName
+)}Store.ts`
+const actionName = `${rootPagePath}/actions/${fisrtToLowercase(
+ fileDirectoryName
+)}Action.ts`
+
+removeFileOrDirectory(pagePath)
+removeFileOrDirectory(storeName)
+removeFileOrDirectory(actionName)
+
+ChildProcess.execFile(
+ 'node',
+ [Path.join(__dirname, './sync-mobx.js')],
+ (error, stdout, stderr) => {
+ if (error) {
+ console.error('stderr', stderr)
+ throw error
+ }
+
+ console.log(Chalk.cyan(stdout))
+ }
+)
+
+/**
+ * 创建文件
+ * @param path {string} 路径
+ * @defaultFileContent ?{string} 默认文件内容
+ * @return {bollean}
+ */
+function removeFileOrDirectory(path) {
+ if (FS.existsSync(path)) {
+ FS.removeSync(path)
+ console.log(Chalk.red('delete file or directory success: ') + path)
+ return true
+ }
+}
+
+function fisrtToLowercase(str) {
+ return str.charAt(0).toLowerCase() + str.substr(1)
+}
diff --git a/src/tools/sync-mobx.js b/src/tools/sync-mobx.js
new file mode 100644
index 0000000..76fef6d
--- /dev/null
+++ b/src/tools/sync-mobx.js
@@ -0,0 +1,177 @@
+const FS = require('fs-extra')
+const Path = require('path')
+const Chalk = require('chalk')
+const listFile = require('./utils/io').listFiles
+
+const packageJson = FS.readJSONSync(Path.join(process.cwd(), 'package.json'), {
+ encoding: 'utf-8',
+})
+const { basePath } = packageJson
+const storesPath = listFile(
+ basePath,
+ /(.+\/)*(stores|globalStores)\/(.+)\.(t|j)s$/g
+)
+// 所有 store 的 path 数组
+const actionsPath = listFile(basePath, /(.+\/)*actions\/(.+)\.(t|j)s$/g)
+// 所有 action 的 path 数组
+const mobxDependencePath = Path.join(process.cwd(), basePath) // ./src
+const mobxTypingsPath = Path.join(process.cwd(), basePath, 'typings') // ./src/typings
+
+createDependenceFile()
+createTypingsFile()
+
+function createDependenceFile() {
+ const content = createDependenceImportContent(mobxDependencePath)
+
+ createFile(Path.join(mobxDependencePath, 'mobxDependence.ts'), content)
+}
+
+function createTypingsFile() {
+ const importContent = createTypingsImportContent(mobxTypingsPath)
+ const IRootStoreContent = generatorIRootStore()
+ const IRootActionContent = generatorIRootAction()
+ const IInjectContent =
+ 'export interface IInject {\n' +
+ ' rootStore: IRootStore\n' +
+ ' rootAction: IRootAction\n' +
+ '}'
+
+ const mobxReactModuleDeclare =
+ 'declare module \'mobx-react\' {' +
+ '\n' +
+ ' export type IValueMapSelf = IStoresToProps' +
+ '\n\n' +
+ ' export function inject(' +
+ '\n' +
+ ' fn: IStoresToProps' +
+ '\n' +
+ ' ): (target: T) => T & IWrappedComponent' +
+ '\n' +
+ '}'
+
+ const content = `${importContent}\n${IRootStoreContent}\n\n${IRootActionContent}\n\n${IInjectContent}\n\n${mobxReactModuleDeclare}\n`
+
+ createFile(Path.join(mobxTypingsPath, 'index.d.ts'), content)
+}
+
+function generatorIRootStore() {
+ const pathMap = storesPath.reduce((ret, path) => {
+ const pathArr = getFileNameFrom(path)
+ ret[pathArr[0]] = ret[pathArr[0]] || []
+ ret[pathArr[0]].push(pathArr[1])
+ return ret
+ }, {})
+ // console.log(pathMap)
+ let content = 'export interface IRootStore {\n'
+ content += Object.keys(pathMap).reduce((ret, pageName) => {
+ ret += ` ${pageName}: {\n`
+ const storeArr = pathMap[pageName]
+ // console.log(storeArr)
+ storeArr.forEach(storeName => {
+ ret += ` ${storeName}: ${pageName}${firstToUppercase(storeName)}\n`
+ })
+ ret += ' }\n'
+ return ret
+ }, '')
+
+ content += '}'
+ return content
+}
+
+function generatorIRootAction() {
+ const pathMap = actionsPath.reduce((ret, path) => {
+ const pathArr = getFileNameFrom(path)
+ ret[pathArr[0]] = ret[pathArr[0]] || []
+ ret[pathArr[0]].push(pathArr[1])
+ return ret
+ }, {})
+ let content = 'export interface IRootAction {\n'
+ content += Object.keys(pathMap).reduce((ret, pageName) => {
+ ret += ` ${pageName}: {\n`
+ const actionArr = pathMap[pageName]
+ actionArr.forEach(actionName => {
+ ret += ` ${actionName}: ${pageName}${firstToUppercase(actionName)}\n`
+ })
+ ret += ' }\n'
+ return ret
+ }, '')
+
+ content += '}'
+ return content
+}
+
+function createDependenceImportContent(base) {
+ const content = storesPath.concat(actionsPath).reduce((content, path) => {
+ const pathArr = getFileNameFrom(path)
+ let relativePath = Path.relative(base, path)
+ if (relativePath.indexOf('.') !== 0) {
+ relativePath = `./${relativePath}`
+ }
+ relativePath = removeExt(relativePath)
+ const importConent = `import '${relativePath}'`
+
+ content += importConent
+ content += '\n'
+ return content
+ }, '')
+ return content
+}
+
+function createTypingsImportContent(base) {
+ let content =
+ 'import { IStoresToProps, IReactComponent, IWrappedComponent } from \'mobx-react\'\n'
+ content += storesPath.concat(actionsPath).reduce((content, path) => {
+ const pathArr = getFileNameFrom(path)
+ // console.log(path, pathArr)
+ let relativePath = Path.relative(base, path)
+ if (relativePath.indexOf('.') !== 0) {
+ relativePath = `./${relativePath}`
+ }
+ relativePath = removeExt(relativePath)
+ const importConent = `import ${pathArr[0]}${firstToUppercase(
+ pathArr[1]
+ )} from '${relativePath}'`
+
+ content += importConent
+ content += '\n'
+ return content
+ }, '')
+ return content
+}
+
+function getFileNameFrom(path) {
+ const reg = /([^\/]+)\/(?:actions|stores|globalStores)\/(.+)\.(?:t|j)sx?$/
+ const matched = path.match(reg)
+ let pageName = matched && matched[1]
+ const fileName = matched && matched[2]
+ if (path.indexOf('globalStores') > -1) pageName = 'globalStores'
+ return [pageName, fileName]
+}
+
+function firstToUppercase(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1)
+}
+
+function removeExt(path) {
+ const reg = /(.+)\.[^\.]+$/
+ return path.replace(reg, '$1')
+}
+
+/**
+ * 创建文件
+ * @param path {string} 路径
+ * @defaultFileContent ?{string} 默认文件内容
+ * @return {bollean}
+ */
+
+function createFile(path, defaultFileContent) {
+ defaultFileContent = defaultFileContent || ''
+ const existed = FS.existsSync(path)
+ FS.outputFileSync(path, defaultFileContent)
+ if (existed) {
+ console.log(Chalk.blue(`updated "${path}".`))
+ } else {
+ console.log(Chalk.blue(`created "${path}".`))
+ }
+ return true
+}
diff --git a/src/tools/templates/actions/action.ts.tpl b/src/tools/templates/actions/action.ts.tpl
new file mode 100644
index 0000000..3ce6d7b
--- /dev/null
+++ b/src/tools/templates/actions/action.ts.tpl
@@ -0,0 +1,11 @@
+import { mAction } from '../../../mobx/action'
+import { IRootAction, IRootStore } from '../../../typings'
+
+@mAction
+export default class ${uppercaseName}$Action {
+ constructor(
+ public stores: IRootStore['${rootPageName}$'],
+ public actions: IRootAction['${rootPageName}$'],
+ ) {}
+
+}
diff --git a/src/tools/templates/index.scss.tpl b/src/tools/templates/index.scss.tpl
new file mode 100644
index 0000000..c361502
--- /dev/null
+++ b/src/tools/templates/index.scss.tpl
@@ -0,0 +1,5 @@
+$prefixCls: '${type}$-${splitDashName}$';
+
+.#{$prefixCls} {
+
+}
diff --git a/src/tools/templates/index.tsx.tpl b/src/tools/templates/index.tsx.tpl
new file mode 100644
index 0000000..3b61472
--- /dev/null
+++ b/src/tools/templates/index.tsx.tpl
@@ -0,0 +1,50 @@
+import * as React from 'react'
+import { inject, observer } from 'mobx-react'
+import { IRootStore, IRootAction } from '${relDir}$typings'
+
+import './index.scss'
+
+@inject(injector)
+@observer
+export default class ${uppercaseName}$ extends React.Component {
+ static defaultProps = {
+ prefixCls: '${type}$-${splitDashName}$',
+ }
+
+ constructor(props: IProps) {
+ super(props)
+ }
+
+ componentDidMount() {}
+
+ render() {
+ const { prefixCls } = this.props
+
+ return (
+
+ this is ${splitDashName}$
+
+ )
+ }
+}
+
+type injectorReturnType = ReturnType
+
+interface IProps extends Partial {
+ prefixCls?: string
+ [k: string]: any
+}
+
+interface IState extends Partial {
+ [k: string]: any
+}
+
+function injector({
+ rootStore,
+ rootAction,
+}: {
+ rootStore: IRootStore,
+ rootAction: IRootAction,
+}) {
+ return {}
+}
diff --git a/src/tools/templates/stores/store.ts.tpl b/src/tools/templates/stores/store.ts.tpl
new file mode 100644
index 0000000..0d5a8f3
--- /dev/null
+++ b/src/tools/templates/stores/store.ts.tpl
@@ -0,0 +1,11 @@
+import { action, observable, computed } from 'mobx'
+import { mStore } from '../../../mobx/store'
+
+interface ISomething {
+
+}
+
+@mStore
+export default class ${uppercaseName}$Store {
+
+}
diff --git a/src/tools/utils/io.js b/src/tools/utils/io.js
new file mode 100644
index 0000000..0923ce9
--- /dev/null
+++ b/src/tools/utils/io.js
@@ -0,0 +1,22 @@
+const FS = require('fs')
+const Path = require('path')
+
+exports.listFiles = function listFiles(path, regex = /./) {
+ const names = FS.readdirSync(path)
+ const files = []
+ const dirs = []
+ for (const name of names) {
+ const targetPath = Path.join(path, name)
+ if (FS.statSync(targetPath).isFile()) {
+ if (targetPath.match(regex)) {
+ files.push(targetPath)
+ }
+ } else {
+ dirs.push(targetPath)
+ }
+ }
+ for (const dir of dirs) {
+ files.push.apply(files, listFiles(dir, regex))
+ }
+ return files
+}
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
new file mode 100644
index 0000000..792640f
--- /dev/null
+++ b/src/typings/index.d.ts
@@ -0,0 +1,28 @@
+import { IStoresToProps, IReactComponent, IWrappedComponent } from 'mobx-react'
+import ExampleExampleStore from '../pages/Example/stores/exampleStore'
+import ExampleExampleAction from '../pages/Example/actions/exampleAction'
+
+export interface IRootStore {
+ Example: {
+ exampleStore: ExampleExampleStore
+ }
+}
+
+export interface IRootAction {
+ Example: {
+ exampleAction: ExampleExampleAction
+ }
+}
+
+export interface IInject {
+ rootStore: IRootStore
+ rootAction: IRootAction
+}
+
+declare module 'mobx-react' {
+ export type IValueMapSelf = IStoresToProps
+
+ export function inject(
+ fn: IStoresToProps
+ ): (target: T) => T & IWrappedComponent
+}
diff --git a/src/typings/uid.d.ts b/src/typings/uid.d.ts
new file mode 100644
index 0000000..b78aa70
--- /dev/null
+++ b/src/typings/uid.d.ts
@@ -0,0 +1,2 @@
+export = index;
+declare function index(len: any): any;
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..3719737
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,16 @@
+import uid from 'uid';
+import { PREFIXCLS } from 'common';
+import request from './request';
+
+function getUid(len = 10) {
+ return `${PREFIXCLS}-${uid(len)}`;
+}
+
+// tslint:disable-next-line: no-empty
+function emptyFn() {}
+
+function getLocalDate(date: Date) {
+ return date.toLocaleString('zh', { hour12: false }).replace(/\//g, '-');
+}
+
+export { getUid, emptyFn, request, getLocalDate };
diff --git a/src/utils/request.ts b/src/utils/request.ts
new file mode 100644
index 0000000..1306fc3
--- /dev/null
+++ b/src/utils/request.ts
@@ -0,0 +1,189 @@
+import axios, { AxiosRequestConfig, AxiosInstance, AxiosResponse, Canceler } from 'axios'
+import qs from 'qs'
+import { createBrowserHistory } from 'history'
+import { Toast } from 'antd-mobile'
+import { DELAY_TIME, API_URL } from 'common'
+import { IReqOptions, IResponse } from '../interface'
+
+// examples:
+// 1. get baseUrl/users/Dolor await request.setPath('users').get({ uri: 'Dolor' })
+
+// 2. delete baseUrl/users/Dolor await request.setPath('users').delete({ uri: 'Dolor' })
+
+// 3. post baseUrl/users await request.setPath('users').post({ data: { name: 'lawler', email: 'lawler61@163.com' })
+
+// 4. put baseUrl/users await request.setPath('users').put({ uri: 'Dolor', data: { name: 'Bolor' } })
+
+const defaultOptions: AxiosRequestConfig = {
+ baseURL: API_URL,
+ timeout: 6000,
+ withCredentials: true,
+ headers: {
+ 'Content-Type': 'application/json;charset=utf-8', // 跨域的时候会产生 options预检
+ },
+}
+
+const { CancelToken } = axios
+
+class Request {
+ [x: string]: any
+
+ static instance: Request
+
+ request: AxiosInstance
+
+ cancel: Canceler
+
+ methods = ['get', 'post', 'put', 'patch', 'delete']
+
+ path = ''
+
+ curPath = ''
+
+ constructor(public history: any, options: AxiosRequestConfig) {
+ this.request = axios.create(options)
+
+ this.methods.forEach(method => {
+ this[method] = (params: IReqOptions) => this.getRequest(method, params)
+ })
+
+ this.upload = (data: object, callback: (process: any) => void) => this.getUploader(data, callback)
+
+ this.initInterceptors()
+ }
+
+ static getInstance(history = createBrowserHistory(), options = defaultOptions) {
+ if (!this.instance) {
+ this.instance = new Request(history, options)
+ }
+ return this.instance
+ }
+
+ initInterceptors() {
+ this.request.interceptors.request.use((config: AxiosRequestConfig) => {
+ const token = localStorage.getItem('token')
+
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+
+ return config
+ })
+
+ this.request.interceptors.response.use(
+ (res: AxiosResponse) => {
+ const { data } = res
+
+ if (data.status === 401) {
+ Toast.fail('认证过期,请登录后再操作!', DELAY_TIME)
+
+ this.history.push('/login')
+ }
+
+ return res
+ },
+ err => {
+ throw err
+ }
+ )
+ }
+
+ /**
+ *
+ *
+ * @param {string} method
+ * @param {string} [options={ uri: '', query: null, data: null }]
+ * @param {string} [options.uri=''] 资源唯一标示,一般是 ID
+ * @param {Object} [options.query=null] GET 参数
+ * @param {Object} [options.data=null] POST/PUT/PATCH 数据
+ * @returns {Promise}
+ */
+ async getRequest(method: string, options: IReqOptions = { uri: '', query: null, data: {} }): Promise {
+ const { uri, query, data } = options
+
+ let url = this.curPath + (uri ? `/${uri}` : '')
+ url += query ? `?${qs.stringify(query)}` : ''
+
+ let result: any = {}
+
+ if (data && data.cancelToken) {
+ // An executor function receives a cancel function as a parameter
+ data.cancelToken = new CancelToken(c => (this.cancel = c))
+ }
+
+ try {
+ const { data: response } = await this.request[method](url, data)
+ // console.log(response)
+ const { errcode, errmsg } = response
+
+ if (!errcode) {
+ result = response
+ } else {
+ throw new Error(`${errcode}: ${errmsg}`)
+ }
+ } catch (err) {
+ Toast.fail(err.message, DELAY_TIME)
+ console.error(err)
+ }
+
+ return result
+ }
+
+ async getUploader(data: any = {}, callback: (process: any) => void): Promise {
+ let result: any = {}
+
+ if (data && data.cancelToken) {
+ // An executor function receives a cancel function as a parameter
+ data.cancelToken = new CancelToken(c => (this.cancel = c))
+ }
+
+ try {
+ const response: IResponse = await this.request.put('/upload', data, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent: any) => {
+ const { loaded, total } = progressEvent
+
+ // callback(((loaded * 100) / total).toFixed(2))
+ callback(`${Math.round((loaded * 10000) / total) / 100}%`)
+ },
+ })
+
+ const { status, data: res } = response
+ const { errcode, errmsg } = res
+
+ if (status === 200 && !errcode) {
+ result = res
+ } else {
+ throw new Error(`${errcode}: ${errmsg}`)
+ }
+ } catch (err) {
+ Toast.fail(err.toString(), DELAY_TIME)
+ console.error(err)
+ }
+
+ return result
+ }
+
+ setPath(...paths: string[]) {
+ this.curPath = `${this.path}/${paths.join('/')}`
+
+ return this
+ }
+
+ /**
+ * 替换链接参数
+ *
+ * @param {...string[]} params
+ */
+ replace(...params: string[]) {
+ let count = 0
+ // questions/{id}/details/{ab}/c
+ this.curPath = this.curPath.replace(/\{.*?\}/g, _match => params[count++])
+
+ return this
+ }
+}
+
+export default Request.getInstance()
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..d63ece8
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,63 @@
+{
+ "compilerOptions": {
+ "baseUrl": "./src",
+ "paths": {
+ "*": [
+ "*",
+ "../node_modules/@types",
+ "typings/*"
+ ]
+ },
+ "target": "es6",
+ "module": "esnext",
+ "lib": [
+ "es6",
+ "es7",
+ "esnext",
+ "dom"
+ ],
+ "types": [
+ "reflect-metadata",
+ "node"
+ ],
+ "sourceMap": true,
+ "jsx": "react",
+ "importHelpers": true,
+ "skipLibCheck": true,
+ "preserveSymlinks": true,
+ "strict": true,
+ /* Enable all strict type-checking options. */
+ "noImplicitAny": true,
+ /* Raise error on expressions and declarations with an implied 'any' type. */
+ "strictNullChecks": true,
+ /* Enable strict null checks. */
+ "strictPropertyInitialization": false,
+ "noImplicitThis": true,
+ /* Raise error on 'this' expressions with an implied 'any' type. */
+ "alwaysStrict": true,
+ /* Parse in strict mode and emit "use strict" for each source file. */
+ /* Additional Checks */
+ "noUnusedLocals": true,
+ /* Report errors on unused locals. */
+ "noUnusedParameters": true,
+ /* Report errors on unused parameters. */
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ /* Report error when not all code paths in function return a value. */
+ "noEmitOnError": false,
+ /* Module Resolution Options */
+ "pretty": true,
+ "allowSyntheticDefaultImports": true,
+ "allowUnusedLabels": false,
+ "esModuleInterop": true,
+ "moduleResolution": "node",
+ /* Experimental Options */
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true
+ },
+ "compileOnSave": false,
+ "exclude": [
+ "node_modules",
+ "src/typings"
+ ]
+}
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 0000000..8d93a66
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,177 @@
+{
+ "extends": [
+ "tslint:recommended",
+ "tslint-react",
+ "tslint-eslint-rules",
+ "tslint-config-prettier"
+ ],
+ "rules": {
+ "align": [
+ false
+ ],
+ "ban": false,
+ "class-name": true,
+ "comment-format": [
+ true,
+ "check-space"
+ ],
+ "curly": true,
+ "eofline": true,
+ "forin": true,
+ "indent": false,
+ "interface-name": true,
+ "label-position": true,
+ "max-line-length": [
+ true,
+ 180
+ ],
+ "member-access": false,
+ "member-ordering": [
+ true,
+ {
+ "order": [
+ "private-static-field",
+ "public-static-field",
+ "private-instance-field",
+ "public-instance-field",
+ "private-constructor",
+ "public-constructor",
+ "private-instance-method",
+ "protected-instance-method",
+ "public-static-method",
+ "public-instance-method"
+ ]
+ }
+ ],
+ "ordered-imports": false,
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-conditional-assignment": true,
+ "no-consecutive-blank-lines": true,
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-variable": true,
+ "no-empty": true,
+ "no-eval": true,
+ "no-inferrable-types": false,
+ "no-internal-module": true,
+ "no-string-literal": true,
+ "no-switch-case-fall-through": true,
+ "no-trailing-whitespace": true,
+ "no-unused-expression": true,
+ "no-use-before-declare": true,
+ "no-var-keyword": true,
+ "no-var-requires": true,
+ "no-angle-bracket-type-assertion": false,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "quotemark": [
+ true,
+ "single",
+ "jsx-double",
+ "avoid-escape"
+ ],
+ "radix": true,
+ "semicolon": false,
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "typedef-whitespace": [
+ true,
+ {
+ "call-signature": "nospace",
+ "index-signature": "nospace",
+ "parameter": "nospace",
+ "property-declaration": "nospace",
+ "variable-declaration": "nospace"
+ }
+ ],
+ "variable-name": [
+ true,
+ "ban-keywords"
+ ],
+ "arrow-parens": [
+ true,
+ "ban-single-arg-parens"
+ ],
+ "object-literal-sort-keys": false,
+ "object-curly-spacing": [
+ true,
+ "always"
+ ],
+ "object-literal-key-quotes": false,
+ "max-classes-per-file": [
+ true,
+ 5,
+ "exclude-class-expressions"
+ ],
+ "interface-over-type-literal": false,
+ "jsdoc-format": false,
+ "new-parens": true,
+ "no-console": [
+ true,
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-multi-spaces": true,
+ "no-namespace": true,
+ "no-reference": true,
+ "no-shadowed-variable": true,
+ "one-variable-per-declaration": [
+ true,
+ "ignore-for-loop"
+ ],
+ "prefer-const": [
+ true,
+ {
+ "destructuring": "all"
+ }
+ ],
+ "space-in-parens": true,
+ "switch-default": true,
+ "trailing-comma": [
+ true,
+ {
+ "singleline": "never",
+ "multiline": {
+ "objects": "always",
+ "arrays": "always",
+ "imports": "always",
+ "exports": "always",
+ "functions": "never"
+ },
+ "esSpecCompliant": true
+ }
+ ],
+ "use-isnan": true,
+ "jsx-no-lambda": false,
+ "jsx-no-string-ref": false,
+ "jsx-boolean-value": [
+ true,
+ "never"
+ ],
+ "jsx-no-multiline-js": false,
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-module",
+ "check-separator",
+ "check-rest-spread",
+ "check-type",
+ "check-typecast",
+ "check-type-operator",
+ "check-preblock"
+ ]
+ }
+}