├── .babelrc ├── .codeclimate.yml ├── .csslintrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── _config.yml ├── config ├── commons.js ├── jest.css.stub.js ├── jest.setup.js ├── webpack.dev.config.js └── webpack.prod.config.js ├── docs └── record.gif ├── jest.config.js ├── package.json ├── postcss.config.js ├── scripts └── assets.js ├── src ├── assets │ ├── favicon.ico │ └── svg │ │ └── codelines.svg ├── client │ ├── components │ │ ├── common │ │ │ ├── Navigator.jsx │ │ │ ├── RouteWithSubRoutes.jsx │ │ │ ├── TextInput.jsx │ │ │ ├── __tests__ │ │ │ │ ├── Navigator.spec.js │ │ │ │ ├── TextInput.spec.js │ │ │ │ └── __snapshots__ │ │ │ │ │ ├── Navigator.spec.js.snap │ │ │ │ │ └── TextInput.spec.js.snap │ │ │ └── styles │ │ │ │ ├── Navigator.less │ │ │ │ └── TextInput.less │ │ └── repository │ │ │ ├── Entry.jsx │ │ │ ├── Languages.jsx │ │ │ ├── List.jsx │ │ │ ├── Main.jsx │ │ │ ├── Tabs.jsx │ │ │ ├── __tests__ │ │ │ ├── Entry.spec.js │ │ │ ├── Languages.spec.js │ │ │ ├── List.spec.js │ │ │ ├── Main.spec.js │ │ │ ├── Tabs.spec.js │ │ │ └── __snapshots__ │ │ │ │ ├── Entry.spec.js.snap │ │ │ │ ├── Languages.spec.js.snap │ │ │ │ ├── List.spec.js.snap │ │ │ │ ├── Main.spec.js.snap │ │ │ │ └── Tabs.spec.js.snap │ │ │ └── styles │ │ │ ├── Entry.less │ │ │ ├── Languages.less │ │ │ ├── List.less │ │ │ ├── Main.less │ │ │ └── Tabs.less │ ├── containers │ │ ├── Home.jsx │ │ ├── Repository.jsx │ │ ├── __tests__ │ │ │ ├── Home.spec.js │ │ │ ├── Repository.spec.js │ │ │ └── __snapshots__ │ │ │ │ ├── Home.spec.js.snap │ │ │ │ └── Repository.spec.js.snap │ │ └── styles │ │ │ ├── Home.less │ │ │ └── Repository.less │ ├── index.jsx │ ├── models │ │ ├── actions │ │ │ ├── __tests__ │ │ │ │ └── create.spec.js │ │ │ ├── create.js │ │ │ └── repository.js │ │ ├── reducers │ │ │ ├── createReducer.js │ │ │ ├── index.js │ │ │ └── repository.js │ │ └── store │ │ │ └── index.js │ └── routes │ │ └── index.jsx ├── development.js ├── production.js ├── server │ ├── .eslintrc │ └── infrastructure │ │ ├── middlewares │ │ ├── favicon.js │ │ ├── index.js │ │ ├── logger.js │ │ ├── render.jsx │ │ ├── statics.js │ │ └── views.js │ │ └── templates │ │ ├── 200.ejs │ │ ├── 404.ejs │ │ └── 500.ejs ├── shared │ ├── base │ │ └── exceptions │ │ │ └── RequestError.js │ ├── http │ │ └── api.js │ ├── test │ │ └── index.js │ └── utils │ │ ├── request.js │ │ └── url.js └── themes │ └── default.less └── wallaby.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react", 5 | "stage-0" 6 | ], 7 | "plugins": [ 8 | "transform-async-to-generator", 9 | "universal-import" 10 | ], 11 | "env": { 12 | "development": { 13 | "plugins": ["react-hot-loader/babel"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | csslint: 4 | enabled: true 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - ruby 10 | - javascript 11 | - python 12 | - php 13 | eslint: 14 | enabled: true 15 | fixme: 16 | enabled: true 17 | ratings: 18 | paths: 19 | - "**.css" 20 | - "**.inc" 21 | - "**.js" 22 | - "**.jsx" 23 | - "**.module" 24 | - "**.php" 25 | - "**.py" 26 | - "**.rb" 27 | exclude_paths: 28 | - "build/" 29 | - "config/" 30 | - "coverage/" 31 | - "docs/" 32 | - "node_modules/" 33 | - "scripts/" 34 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | --exclude-exts=.min.css 2 | --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /node_modules/ 4 | /scripts/ 5 | **/__tests__/ 6 | **/statics.js 7 | wallaby.js 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "no-await-in-loop": 0, 6 | "no-shadow": 0, 7 | "import/no-extraneous-dependencies": 0, 8 | "import/prefer-default-export": 0, 9 | "no-underscore-dangle": 0, 10 | "global-require": 0 11 | }, 12 | "globals": { 13 | "__ROOT__": true, 14 | "document": true, 15 | "window": true 16 | } 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | /node_modules 3 | /build 4 | /coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - CODECLIMATE_REPO_TOKEN=28c86855122bc245dd440cd9e39ba32a11eab19562afd4aafa0cfcdb3afdc26f 4 | language: node_js 5 | node_js: 6 | - stable 7 | cache: yarn 8 | before_install: yarn global add greenkeeper-lockfile@1 9 | before_script: greenkeeper-lockfile-update 10 | after_script: greenkeeper-lockfile-upload 11 | script: 12 | - yarn lint && yarn test 13 | - yarn codecov 14 | notifications: 15 | email: false 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kkarczmarczyk/node-yarn 2 | 3 | # if you're in China, please comment above image and uncomment following image :) 4 | 5 | # FROM registry.docker-cn.com/kkarczmarczyk/node-yarn 6 | 7 | EXPOSE 3006 8 | 9 | ENV WORKSPACE=/opt/workspace 10 | 11 | RUN mkdir -p $WORKSPACE 12 | 13 | WORKDIR $WORKSPACE 14 | 15 | ADD package.json $WORKSPACE 16 | 17 | # if you're in China, uncomment following phrase to boost your yarn with cn-npm-registry :) 18 | 19 | # RUN yarn config set registry https://registry.npm.taobao.org 20 | 21 | RUN yarn 22 | 23 | ADD . $WORKSPACE 24 | 25 | CMD yarn docker 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koa React Universal 2 | 3 | [](https://greenkeeper.io/) 4 | [](https://travis-ci.org/kimjuny/koa-react-universal) 5 | [](https://codeclimate.com/github/kimjuny/koa-react-universal) 6 | [](https://codeclimate.com/github/kimjuny/koa-react-universal/coverage) 7 | 8 | > koa2、react、react-universal-component、koa-webpack-server、async/await、code-splitting、hot-module-replacement、react-router4、redux-thunk 9 | 10 | This project is dedicated to build simple yet powerful Koa-React-Universal boilerplate. 11 | 12 | Less is More: All key ingredients are in `src/development`、`src/production` and webpack configurations, easy to understand、set-up and extend. We promise to use the most recent & official packages(as much as we can), no weird or tricky stuffs, keeping this project clean and fully customizable. 13 | 14 | Fully functional: HMR for client and server side, code splitting for both javascript and css, async/await universal programming support for koa2 server-side and redux-thunk client-side... 15 | 16 | Universal: We are using [react-universal-component](https://github.com/faceyspacey/react-universal-component)、[webpack-flush-chunks](https://github.com/faceyspacey/webpack-flush-chunks). It simplifies universal development with code-splitting(js、css) and is also compatible with the latest react-router-v4. 17 | 18 | Production: We are using webpack to build client(target: web) and server(target: node). 19 | 20 | Development: We are using [koa-webpack-server](https://github.com/kimjuny/koa-webpack-server) (which simplifies development env set-ups), it also webpacks client and server(with client & server hot-load), so we can stay as much as the same with production. 21 | 22 | ### Screenshots 23 | 24 | DEMO: Search Github Repositories. 25 | 26 | > Noted: Github search API has [Rate Limitation](https://developer.github.com/v3/search/#rate-limit) of 10 reqs/min (without credentials). 27 | 28 |  29 | 30 | ### Components 31 | 32 | * koa2 33 | * react 34 | * react-router4 35 | * redux-thunk 36 | * react-universal-component 37 | * es2015 + async/await 38 | * less、postcss(autoprefixer) 39 | * webpack 40 | * koa-webpack-server 41 | * webpack-flush-chunks 42 | * axios 43 | * ejs 44 | * jest 45 | * eslint(airbnb) 46 | * docker 47 | * wallaby 48 | 49 | ### Roadmap 50 | 51 | * graphql(Github API v3 -> v4) 52 | * flow 53 | * enzyme 54 | * immutable 55 | * vendor 56 | 57 | ### Start 58 | 59 | #### Prerequisites 60 | 61 | development 62 | 63 | * yarn / npm 64 | * node ≥ 7.0 65 | 66 | production 67 | 68 | * docker ≥ 1.13 69 | 70 | #### Production 71 | 72 | ``` 73 | yarn prod 74 | ``` 75 | 76 | or with docker 77 | 78 | ``` 79 | docker build -t koa-react-universal . 80 | docker run -d -p 3006:3006 koa-react-universal 81 | ``` 82 | 83 | #### Development 84 | 85 | ``` 86 | yarn dev 87 | ``` 88 | 89 | #### Test 90 | 91 | ``` 92 | yarn test 93 | ``` 94 | 95 | also supports [wallaby.js](https://wallabyjs.com/) live test reports [view](http://wallabyjs.com/app/#/files) 96 | 97 | ``` 98 | CMD + SHIFT + R -> R (vscode) 99 | ``` 100 | 101 | ### License 102 | 103 | [MIT](https://github.com/kimjuny/koa-react-universal/blob/master/LICENSE) 104 | 105 | ### Contributing 106 | 107 | Issues are welcome :) 108 | 109 | PRs are welcome (if you can help to make things more concise and simple!). 110 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /config/commons.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | // client webpack config name 5 | client: 'client', 6 | // server webpack config name 7 | server: 'server', 8 | alias: { 9 | assets: path.resolve(__dirname, '../src/assets'), 10 | client: path.resolve(__dirname, '../src/client'), 11 | server: path.resolve(__dirname, '../src/server'), 12 | shared: path.resolve(__dirname, '../src/shared'), 13 | themes: path.resolve(__dirname, '../src/themes'), 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /config/jest.css.stub.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /config/jest.setup.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimjuny/koa-react-universal/4f46ca70b358eb160b90095ecf701298361dae16/config/jest.setup.js -------------------------------------------------------------------------------- /config/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const ExtractCssChunks = require('extract-css-chunks-webpack-plugin'); 5 | const OpenBrowserPlugin = require('open-browser-webpack-plugin'); 6 | const commons = require('./commons'); 7 | 8 | const client = { 9 | name: commons.client, 10 | entry: [ 11 | 'babel-polyfill', 12 | 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=false', 13 | 'react-hot-loader/patch', 14 | path.resolve(__dirname, '../src/client/index.jsx'), 15 | ], 16 | output: { 17 | publicPath: '/build/client/', 18 | filename: '[name].js', 19 | chunkFilename: '[name].js', 20 | }, 21 | resolve: { 22 | alias: commons.alias, 23 | extensions: ['.js', '.jsx', '.less', '.css'], 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(js|jsx)$/, 29 | exclude: /node_modules/, 30 | loader: 'babel-loader', 31 | }, { 32 | test: /\.less$/, 33 | use: ExtractCssChunks.extract({ 34 | use: [ 35 | { 36 | loader: 'css-loader', 37 | options: { 38 | localIdentName: '[name]__[local]--[hash:base64:5]', 39 | }, 40 | }, { 41 | loader: 'postcss-loader', 42 | }, { 43 | loader: 'less-loader', 44 | }, 45 | ], 46 | }), 47 | }, { 48 | test: /\.svg$/, 49 | exclude: /node_modules/, 50 | loader: 'file-loader', 51 | }, 52 | ], 53 | }, 54 | plugins: [ 55 | new ExtractCssChunks(), 56 | // 'ReferenceError: webpackJsonp is not defined' if this plugin is left out. 57 | new webpack.optimize.CommonsChunkPlugin({ 58 | names: ['bootstrap'], // needed to put webpack bootstrap code before chunks. 59 | filename: '[name].js', 60 | minChunks: Infinity 61 | }), 62 | new webpack.HotModuleReplacementPlugin(), 63 | new webpack.NoEmitOnErrorsPlugin(), 64 | new OpenBrowserPlugin({ url: 'http://localhost:3006' }), 65 | ], 66 | }; 67 | 68 | const externals = fs 69 | .readdirSync(path.resolve(__dirname, '../node_modules')) 70 | .filter(x => !/\.bin|react-universal-component|webpack-flush-chunks/.test(x)) 71 | .reduce((externals, mod) => { 72 | externals[mod] = `commonjs ${mod}`; 73 | return externals; 74 | }, {}); 75 | 76 | externals['react-dom/server'] = 'commonjs react-dom/server'; 77 | 78 | const server = { 79 | name: commons.server, 80 | target: 'node', 81 | entry: [ 82 | 'babel-polyfill', 83 | path.resolve(__dirname, '../src/server/infrastructure/middlewares'), 84 | ], 85 | output: { 86 | path: path.resolve(__dirname, '../build/server'), 87 | filename: 'server.js', 88 | libraryTarget: 'commonjs2', 89 | }, 90 | resolve: { 91 | extensions: ['.js', '.jsx'], 92 | }, 93 | externals, 94 | module: { 95 | rules: [ 96 | { 97 | test: /\.(js|jsx)$/, 98 | loader: 'babel-loader', 99 | }, { 100 | test: /\.(less|css|svg)$/, 101 | loader: 'ignore-loader', 102 | }, 103 | ], 104 | }, 105 | plugins: [ 106 | // see: https://github.com/faceyspacey/react-universal-component/issues/10 107 | new webpack.optimize.LimitChunkCountPlugin({ 108 | maxChunks: 1 109 | }), 110 | new webpack.DefinePlugin({ 111 | __ROOT__: JSON.stringify(path.resolve(__dirname, '../')), 112 | }), 113 | ], 114 | }; 115 | 116 | module.exports = [client, server]; 117 | -------------------------------------------------------------------------------- /config/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const ExtractCssChunks = require('extract-css-chunks-webpack-plugin'); 5 | const commons = require('./commons'); 6 | 7 | const client = { 8 | name: commons.client, 9 | entry: [ 10 | 'babel-polyfill', 11 | path.resolve(__dirname, '../src/client/index.jsx'), 12 | ], 13 | output: { 14 | path: path.resolve(__dirname, '../build/client'), 15 | publicPath: '/build/client/', 16 | filename: '[name].[chunkhash].js', 17 | chunkFilename: '[name].[chunkhash].js', 18 | }, 19 | resolve: { 20 | alias: commons.alias, 21 | extensions: ['.js', '.jsx', '.less', '.css'], 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(js|jsx)$/, 27 | loader: 'babel-loader', 28 | }, { 29 | test: /\.less$/, 30 | use: ExtractCssChunks.extract({ 31 | use: [ 32 | { 33 | loader: 'css-loader', 34 | options: { 35 | localIdentName: '[name]__[local]--[hash:base64:5]', 36 | }, 37 | }, { 38 | loader: 'less-loader', 39 | }, 40 | ], 41 | }), 42 | }, { 43 | test: /\.svg$/, 44 | exclude: /node_modules/, 45 | loader: 'file-loader', 46 | }, 47 | ], 48 | }, 49 | plugins: [ 50 | new ExtractCssChunks(), 51 | // 'ReferenceError: webpackJsonp is not defined' if this plugin is left out. 52 | new webpack.optimize.CommonsChunkPlugin({ 53 | names: ['bootstrap'], // needed to put webpack bootstrap code before chunks. 54 | filename: '[name].[chunkhash].js', 55 | minChunks: Infinity 56 | }), 57 | new webpack.optimize.UglifyJsPlugin({ 58 | compress: { 59 | warnings: false 60 | } 61 | }), 62 | ], 63 | }; 64 | 65 | const externals = fs 66 | .readdirSync(path.resolve(__dirname, '../node_modules')) 67 | .filter(x => !/\.bin|react-universal-component|webpack-flush-chunks/.test(x)) 68 | .reduce((externals, mod) => { 69 | externals[mod] = `commonjs ${mod}` 70 | return externals 71 | }, {}); 72 | 73 | externals['react-dom/server'] = 'commonjs react-dom/server'; 74 | 75 | const server = { 76 | name: commons.server, 77 | target: 'node', 78 | entry: [ 79 | 'babel-polyfill', 80 | path.resolve(__dirname, '../src/server/infrastructure/middlewares'), 81 | ], 82 | output: { 83 | path: path.resolve(__dirname, '../build/server'), 84 | filename: 'server.js', 85 | libraryTarget: 'commonjs2', 86 | }, 87 | resolve: { 88 | extensions: ['.js', '.jsx'], 89 | }, 90 | externals, 91 | module: { 92 | rules: [ 93 | { 94 | test: /\.(js|jsx)$/, 95 | loader: 'babel-loader', 96 | }, { 97 | test: /\.(css|less|svg)$/, 98 | loader: 'ignore-loader', 99 | }, 100 | ], 101 | }, 102 | plugins: [ 103 | // see: https://github.com/faceyspacey/react-universal-component/issues/10 104 | new webpack.optimize.LimitChunkCountPlugin({ 105 | maxChunks: 1 106 | }), 107 | new webpack.DefinePlugin({ 108 | __ROOT__: JSON.stringify(path.resolve(__dirname, '../')), 109 | }), 110 | new webpack.optimize.UglifyJsPlugin({ 111 | compress: { 112 | warnings: false 113 | } 114 | }), 115 | ], 116 | }; 117 | 118 | module.exports = [client, server]; 119 | -------------------------------------------------------------------------------- /docs/record.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimjuny/koa-react-universal/4f46ca70b358eb160b90095ecf701298361dae16/docs/record.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | notify: true, 3 | moduleNameMapper: { 4 | '\\.(css|less)$': '/config/jest.css.stub.js', 5 | }, 6 | collectCoverage: true, 7 | coveragePathIgnorePatterns: [ 8 | '/config/', 9 | '/build/', 10 | '/coverage/', 11 | '/docs/', 12 | '/node_modules/', 13 | '/scripts/', 14 | ], 15 | coverageDirectory: 'coverage', 16 | coverageReporters: ['lcov', 'text-summary'], 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "kimjuny@foxmail.com", 3 | "license": "MIT", 4 | "repository": "https://github.com/kimjuny/koa-react-universa", 5 | "scripts": { 6 | "clean": "rimraf build", 7 | "build": "npm run clean && cross-env NODE_ENV=production webpack --colors --progress --config ./config/webpack.prod.config.js", 8 | "prod": "npm run clean && npm run docker", 9 | "docker": "cross-env NODE_ENV=production PORT=3006 node ./src/production", 10 | "dev": "cross-env NODE_ENV=development PORT=3006 node ./src/development", 11 | "lint": "./node_modules/.bin/eslint src", 12 | "test": "./node_modules/.bin/jest --config jest.config.js --no-cache", 13 | "codecov": "codeclimate-test-reporter < coverage/lcov.info" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.19.0", 17 | "classnames": "^2.2.5", 18 | "colors": "^1.1.2", 19 | "ejs": "^2.5.7", 20 | "koa": "^2.7.0", 21 | "koa-favicon": "^2.0.0", 22 | "koa-send": "^4.1.0", 23 | "koa-views": "^6.0.2", 24 | "lodash": "^4.17.4", 25 | "react": "^16.8.6", 26 | "react-dom": "^16.8.6", 27 | "react-redux": "^7.1.0", 28 | "react-router": "^4.2.0", 29 | "react-router-dom": "^4.2.2", 30 | "react-universal-component": "^2.3.2", 31 | "redux": "^4.0.1", 32 | "redux-thunk": "^2.3.0", 33 | "url-parse": "^1.1.9", 34 | "webpack-flush-chunks": "^1.1.22" 35 | }, 36 | "devDependencies": { 37 | "autoprefixer": "^9.6.0", 38 | "babel-core": "^6.25.0", 39 | "babel-eslint": "^8.0.0", 40 | "babel-loader": "^7.1.1", 41 | "babel-plugin-transform-async-to-generator": "^6.24.1", 42 | "babel-plugin-universal-import": "^1.2.5", 43 | "babel-polyfill": "^6.23.0", 44 | "babel-preset-env": "^1.7.0", 45 | "babel-preset-react": "^6.24.1", 46 | "babel-preset-stage-0": "^6.24.1", 47 | "codeclimate-test-reporter": "^0.5.0", 48 | "cross-env": "^5.0.5", 49 | "css-loader": "^0.28.11", 50 | "eslint": "^5.3.0", 51 | "eslint-config-airbnb": "^17.1.0", 52 | "eslint-plugin-import": "^2.7.0", 53 | "eslint-plugin-jsx-a11y": "^6.1.1", 54 | "eslint-plugin-react": "^7.2.1", 55 | "extract-css-chunks-webpack-plugin": "^2.0.16", 56 | "file-loader": "^1.1.0", 57 | "fs-extra": "^5.0.0", 58 | "ignore-loader": "^0.1.2", 59 | "jest": "^21.0.0", 60 | "koa-webpack-server": "^0.2.4", 61 | "less": "^3.9.0", 62 | "less-loader": "^5.0.0", 63 | "open-browser-webpack-plugin": "^0.0.5", 64 | "postcss-loader": "^3.0.0", 65 | "react-hot-loader": "next", 66 | "react-test-renderer": "^16.8.6", 67 | "rimraf": "^2.6.1", 68 | "webpack": "^3.5.3" 69 | }, 70 | "jest": {} 71 | } 72 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /scripts/assets.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | 3 | fs.removeSync('./build'); 4 | 5 | fs.copySync('./src/assets', './build/assets'); 6 | 7 | console.log('done'); 8 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimjuny/koa-react-universal/4f46ca70b358eb160b90095ecf701298361dae16/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/svg/codelines.svg: -------------------------------------------------------------------------------- 1 | simple-codelines -------------------------------------------------------------------------------- /src/client/components/common/Navigator.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import PropTypes from 'prop-types'; 5 | import TextInput from './TextInput'; 6 | import { listRepositories } from '../../models/actions/repository'; 7 | import { objectToQueryString } from '../../../shared/utils/url'; 8 | import './styles/Navigator.less'; 9 | 10 | class Navigator extends React.Component { 11 | static contextTypes = { 12 | router: PropTypes.object, 13 | } 14 | 15 | constructor() { 16 | super(); 17 | this.state = { value: '' }; 18 | this.onChange = this.onChange.bind(this); 19 | this.onEnter = this.onEnter.bind(this); 20 | } 21 | 22 | onChange(event) { 23 | this.setState({ 24 | value: event.target.value, 25 | }); 26 | } 27 | 28 | onEnter() { 29 | const { dispatch } = this.props; 30 | const query = { 31 | q: this.state.value, 32 | sort: 'stars', 33 | order: 'desc', 34 | }; 35 | dispatch(listRepositories({ query })); 36 | this.context.router.history.push(`/repositories${objectToQueryString(query)}`); 37 | } 38 | 39 | render() { 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 57 | 58 | 59 | Pull Requests 60 | Issues 61 | Marketplace 62 | Gist 63 | 64 | 65 | 66 | ); 67 | } 68 | } 69 | 70 | export default connect()(Navigator); 71 | -------------------------------------------------------------------------------- /src/client/components/common/RouteWithSubRoutes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | 4 | const RouteWithSubRoutes = route => ( 5 | } 9 | /> 10 | ); 11 | 12 | export default RouteWithSubRoutes; 13 | -------------------------------------------------------------------------------- /src/client/components/common/TextInput.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import classNames from 'classnames'; 3 | import './styles/TextInput.less'; 4 | 5 | class TextInput extends PureComponent { 6 | constructor() { 7 | super(); 8 | this.onChange = this.onChange.bind(this); 9 | this.onKeyPress = this.onKeyPress.bind(this); 10 | } 11 | 12 | onChange(event) { 13 | const { onChange } = this.props; 14 | if (typeof onChange === 'function') { 15 | onChange(event); 16 | } 17 | } 18 | 19 | onKeyPress(event) { 20 | const { onEnter } = this.props; 21 | if (typeof onEnter === 'function' && event.key === 'Enter') { 22 | onEnter(); 23 | } 24 | } 25 | 26 | render() { 27 | const { placeholder, value, size, theme } = this.props; 28 | 29 | const className = classNames({ 30 | 'text-input': true, 31 | 'text-input-lg': !size || size === 'lg', 32 | 'text-input-sm': size === 'sm', 33 | 'text-input-white': !theme || theme === 'white', 34 | 'text-input-dark': theme === 'dark', 35 | }); 36 | 37 | return ( 38 | 39 | 47 | 48 | ); 49 | } 50 | } 51 | 52 | export default TextInput; 53 | -------------------------------------------------------------------------------- /src/client/components/common/__tests__/Navigator.spec.js: -------------------------------------------------------------------------------- 1 | import Navigator from '../Navigator'; 2 | import { snapshot } from '../../../../shared/test'; 3 | 4 | describe('components/common/Navigator', () => { 5 | snapshot({ 6 | component: Navigator, 7 | name: 'Navigator', 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/client/components/common/__tests__/TextInput.spec.js: -------------------------------------------------------------------------------- 1 | import TextInput from '../TextInput'; 2 | import { snapshot } from '../../../../shared/test'; 3 | 4 | describe('components/common/TextInput', () => { 5 | snapshot({ 6 | component: TextInput, 7 | name: 'TextInput' 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/client/components/common/__tests__/__snapshots__/Navigator.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/common/Navigator Navigator should render correctly 1`] = ` 4 | 7 | 10 | 15 | 23 | 27 | 28 | 29 | 32 | 33 | 41 | 42 | 43 | 46 | 47 | Pull Requests 48 | 49 | 50 | Issues 51 | 52 | 53 | Marketplace 54 | 55 | 56 | Gist 57 | 58 | 59 | 60 | 61 | `; 62 | -------------------------------------------------------------------------------- /src/client/components/common/__tests__/__snapshots__/TextInput.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/common/TextInput TextInput should render correctly 1`] = ` 4 | 5 | 13 | 14 | `; 15 | -------------------------------------------------------------------------------- /src/client/components/common/styles/Navigator.less: -------------------------------------------------------------------------------- 1 | 2 | .navigator { 3 | background-color: #24292e; 4 | height: 4em; 5 | 6 | .content { 7 | width: 60%; 8 | height: 100%; 9 | margin: 0 auto; 10 | display: flex; 11 | flex-direction: row; 12 | align-items: center; 13 | 14 | .icon { 15 | height: 32px; 16 | width: 32px; 17 | } 18 | 19 | .search { 20 | margin: 0 1em; 21 | } 22 | 23 | .navigation-items { 24 | 25 | a { 26 | color: rgba(255, 255, 255, 0.75); 27 | margin: 0 0.6em; 28 | font-weight: 600; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/client/components/common/styles/TextInput.less: -------------------------------------------------------------------------------- 1 | @import "~themes/default.less"; 2 | 3 | .text-input { 4 | line-height: 20px; 5 | border-style: solid; 6 | padding: 0 10px 0 10px; 7 | outline: none; 8 | } 9 | 10 | .text-input-lg { 11 | height: 40px; 12 | width: 28rem; 13 | font-size: 16px; 14 | border-radius: 5px; 15 | } 16 | 17 | .text-input-sm { 18 | height: 30px; 19 | width: 12rem; 20 | font-size: 14px; 21 | border-radius: 3px; 22 | } 23 | 24 | .text-input-white { 25 | background-color: white; 26 | border-color: #d1d5da; 27 | color: #24292e; 28 | } 29 | 30 | .text-input-dark { 31 | background-color: #3f4448; 32 | border: 0; 33 | color: white; 34 | } 35 | -------------------------------------------------------------------------------- /src/client/components/repository/Entry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './styles/Entry.less'; 3 | 4 | const Entry = ({ repository }) => ( 5 | 6 | 7 | 8 | { repository.full_name } 9 | 10 | 11 | { repository.description } 12 | 13 | 14 | { repository.topics.slice().splice(0, 4).map(topic => {topic}) } 15 | 16 | 17 | { repository.updated_at } 18 | 19 | 20 | 21 | 22 | { repository.language } 23 | 24 | 25 | 26 | 27 | 28 | 29 | { repository.stargazers_count } 30 | 31 | 32 | 33 | ); 34 | 35 | export default Entry; 36 | -------------------------------------------------------------------------------- /src/client/components/repository/Languages.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './styles/Languages.less'; 3 | 4 | const Languages = () => ( 5 | 6 | 7 | Languages 8 | 9 | Javascript 10 | 37524 11 | 12 | 13 | CSS 14 | 2021 15 | 16 | 17 | Typescript 18 | 1838 19 | 20 | 21 | HTML 22 | 1192 23 | 24 | 25 | Vue 26 | 792 27 | 28 | 29 | Ruby 30 | 487 31 | 32 | 33 | PHP 34 | 207 35 | 36 | 37 | C# 38 | 133 39 | 40 | 41 | CoffeeScript 42 | 133 43 | 44 | 45 | Python 46 | 124 47 | 48 | 49 | 50 | ); 51 | 52 | export default Languages; 53 | -------------------------------------------------------------------------------- /src/client/components/repository/List.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Entry from './Entry'; 4 | import './styles/List.less'; 5 | 6 | const List = ({ repositories }) => { 7 | const renderTotal = () => { 8 | if (repositories.data) { 9 | return { repositories.total } repository results; 10 | } 11 | return null; 12 | }; 13 | 14 | const renderList = () => { 15 | if (!repositories.data) { 16 | return null; 17 | } 18 | return repositories.data.map(repository => 19 | ); 20 | }; 21 | 22 | return ( 23 | 24 | { renderTotal() } 25 | { renderList() } 26 | 27 | ); 28 | }; 29 | 30 | const mapStateToProps = state => ({ repositories: state.repository.repositories }); 31 | 32 | export default connect(mapStateToProps)(List); 33 | -------------------------------------------------------------------------------- /src/client/components/repository/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import List from './List'; 3 | import Languages from './Languages'; 4 | import './styles/Main.less'; 5 | 6 | const Main = () => ( 7 | 8 | 9 | 10 | 11 | ); 12 | 13 | export default Main; 14 | -------------------------------------------------------------------------------- /src/client/components/repository/Tabs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import './styles/Tabs.less'; 4 | 5 | const Tabs = ({ repositories }) => ( 6 | 7 | 8 | Repositories48K 9 | Code3M 10 | Commits622K 11 | Issues169K 12 | Wikis10K 13 | Users207 14 | 15 | 16 | ); 17 | 18 | const mapStateToProps = state => ({ repositories: state.repository.repositories }); 19 | 20 | export default connect(mapStateToProps)(Tabs); 21 | -------------------------------------------------------------------------------- /src/client/components/repository/__tests__/Entry.spec.js: -------------------------------------------------------------------------------- 1 | import Entry from '../Entry'; 2 | import { snapshot } from '../../../../shared/test'; 3 | 4 | describe('components/repository/Entry', () => { 5 | snapshot({ 6 | component: Entry, 7 | name: 'Entry', 8 | props: { 9 | repository: { 10 | full_name: 'full_name', 11 | description: 'description', 12 | topics: [], 13 | update_at: 'update_at', 14 | language: 'language', 15 | stargazers_count: 100, 16 | }, 17 | }, 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/client/components/repository/__tests__/Languages.spec.js: -------------------------------------------------------------------------------- 1 | import Languages from '../Languages'; 2 | import { snapshot } from '../../../../shared/test'; 3 | 4 | describe('components/repository/Languages', () => { 5 | snapshot({ 6 | component: Languages, 7 | name: 'Languages', 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/client/components/repository/__tests__/List.spec.js: -------------------------------------------------------------------------------- 1 | import List from '../List'; 2 | import { snapshot } from '../../../../shared/test'; 3 | import configureStore from '../../../models/store'; 4 | 5 | describe('components/repository/List', () => { 6 | const store = configureStore({ 7 | repository: { 8 | repositories: { 9 | total: 1, 10 | data: [ 11 | { 12 | full_name: 'full_name', 13 | description: 'description', 14 | topics: [], 15 | update_at: 'update_at', 16 | language: 'language', 17 | stargazers_count: 100, 18 | }, 19 | ], 20 | }, 21 | }, 22 | }); 23 | 24 | snapshot({ 25 | component: List, 26 | name: 'List', 27 | store, 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/client/components/repository/__tests__/Main.spec.js: -------------------------------------------------------------------------------- 1 | import Main from '../Main'; 2 | import { snapshot } from '../../../../shared/test'; 3 | 4 | describe('components/repository/Main', () => { 5 | snapshot({ 6 | component: Main, 7 | name: 'Main', 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/client/components/repository/__tests__/Tabs.spec.js: -------------------------------------------------------------------------------- 1 | import Tabs from '../Tabs'; 2 | import { snapshot } from '../../../../shared/test'; 3 | 4 | describe('components/repository/Tabs', () => { 5 | snapshot({ 6 | component: Tabs, 7 | name: 'Tabs', 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/client/components/repository/__tests__/__snapshots__/Entry.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/repository/Entry Entry should render correctly 1`] = ` 4 | 7 | 10 | 13 | 16 | full_name 17 | 18 | 19 | 22 | description 23 | 24 | 27 | 30 | 31 | 34 | 37 | 38 | language 39 | 40 | 41 | 44 | 52 | 56 | 57 | 58 | 100 59 | 60 | 61 | 62 | `; 63 | -------------------------------------------------------------------------------- /src/client/components/repository/__tests__/__snapshots__/Languages.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/repository/Languages Languages should render correctly 1`] = ` 4 | 7 | 8 | 9 | Languages 10 | 11 | 12 | 15 | Javascript 16 | 17 | 20 | 37524 21 | 22 | 23 | 24 | 27 | CSS 28 | 29 | 32 | 2021 33 | 34 | 35 | 36 | 39 | Typescript 40 | 41 | 44 | 1838 45 | 46 | 47 | 48 | 51 | HTML 52 | 53 | 56 | 1192 57 | 58 | 59 | 60 | 63 | Vue 64 | 65 | 68 | 792 69 | 70 | 71 | 72 | 75 | Ruby 76 | 77 | 80 | 487 81 | 82 | 83 | 84 | 87 | PHP 88 | 89 | 92 | 207 93 | 94 | 95 | 96 | 99 | C# 100 | 101 | 104 | 133 105 | 106 | 107 | 108 | 111 | CoffeeScript 112 | 113 | 116 | 133 117 | 118 | 119 | 120 | 123 | Python 124 | 125 | 128 | 124 129 | 130 | 131 | 132 | 133 | `; 134 | -------------------------------------------------------------------------------- /src/client/components/repository/__tests__/__snapshots__/List.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/repository/List List should render correctly 1`] = ` 4 | 7 | 8 | 1 9 | repository results 10 | 11 | 14 | 17 | 20 | 23 | full_name 24 | 25 | 26 | 29 | description 30 | 31 | 34 | 37 | 38 | 41 | 44 | 45 | language 46 | 47 | 48 | 51 | 59 | 63 | 64 | 65 | 100 66 | 67 | 68 | 69 | 70 | `; 71 | -------------------------------------------------------------------------------- /src/client/components/repository/__tests__/__snapshots__/Main.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/repository/Main Main should render correctly 1`] = ` 4 | 7 | 10 | 13 | 14 | 15 | Languages 16 | 17 | 18 | 21 | Javascript 22 | 23 | 26 | 37524 27 | 28 | 29 | 30 | 33 | CSS 34 | 35 | 38 | 2021 39 | 40 | 41 | 42 | 45 | Typescript 46 | 47 | 50 | 1838 51 | 52 | 53 | 54 | 57 | HTML 58 | 59 | 62 | 1192 63 | 64 | 65 | 66 | 69 | Vue 70 | 71 | 74 | 792 75 | 76 | 77 | 78 | 81 | Ruby 82 | 83 | 86 | 487 87 | 88 | 89 | 90 | 93 | PHP 94 | 95 | 98 | 207 99 | 100 | 101 | 102 | 105 | C# 106 | 107 | 110 | 133 111 | 112 | 113 | 114 | 117 | CoffeeScript 118 | 119 | 122 | 133 123 | 124 | 125 | 126 | 129 | Python 130 | 131 | 134 | 124 135 | 136 | 137 | 138 | 139 | 140 | `; 141 | -------------------------------------------------------------------------------- /src/client/components/repository/__tests__/__snapshots__/Tabs.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/repository/Tabs Tabs should render correctly 1`] = ` 4 | 7 | 10 | 13 | Repositories 14 | 15 | 48K 16 | 17 | 18 | 21 | Code 22 | 23 | 3M 24 | 25 | 26 | 29 | Commits 30 | 31 | 622K 32 | 33 | 34 | 37 | Issues 38 | 39 | 169K 40 | 41 | 42 | 45 | Wikis 46 | 47 | 10K 48 | 49 | 50 | 53 | Users 54 | 55 | 207 56 | 57 | 58 | 59 | 60 | `; 61 | -------------------------------------------------------------------------------- /src/client/components/repository/styles/Entry.less: -------------------------------------------------------------------------------- 1 | @import "~themes/default.less"; 2 | 3 | .entry { 4 | border-top-style: solid; 5 | border-top-color: rgba(200, 200, 200, 0.6); 6 | border-top-width: 1px; 7 | display: flex; 8 | flex-direction: row; 9 | align-items: flex-start; 10 | 11 | .left { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: space-between; 15 | align-items: left; 16 | width: 67%; 17 | 18 | .full-name { 19 | margin-top: 2em; 20 | 21 | a { 22 | max-width: 19em; 23 | display: -webkit-box; 24 | -webkit-box-orient: vertical; 25 | -webkit-line-clamp: 3; 26 | overflow: hidden; 27 | color: #0366d6; 28 | text-decoration: none; 29 | font-size: 5/3em; 30 | font-weight: 600; 31 | } 32 | } 33 | 34 | .description { 35 | .text-normal; 36 | margin-top: 0.5em; 37 | 38 | max-width: 25em; 39 | display: -webkit-box; 40 | -webkit-box-orient: vertical; 41 | -webkit-line-clamp: 3; 42 | overflow: hidden; 43 | } 44 | 45 | .topics { 46 | display: flex; 47 | margin-top: 0.5em; 48 | 49 | .topic { 50 | .text-normal; 51 | 52 | font-size: 3/4em; 53 | padding: 0.3em 0.9em; 54 | margin: 0 1em 0 0; 55 | color: rgb(3, 102, 214); 56 | background-color: #f1f8ff; 57 | } 58 | } 59 | 60 | .update-time { 61 | .text-normal; 62 | margin: 1em 0 1.8em 0; 63 | } 64 | } 65 | 66 | .middle { 67 | width: 17%; 68 | margin-top: 2.5em; 69 | display: flex; 70 | flex-direction: row; 71 | align-items: center; 72 | 73 | .language { 74 | background-color: #f1e05a; 75 | border-radius: 50%; 76 | width: 1em; 77 | height: 1em; 78 | } 79 | 80 | p { 81 | .text-normal; 82 | margin: 0 0 0 0.2em; 83 | } 84 | } 85 | 86 | .right { 87 | width: 16%; 88 | margin-top: 2.5em; 89 | display: flex; 90 | flex-direction: row; 91 | justify-content: flex-end; 92 | align-items: center; 93 | 94 | p { 95 | .text-normal; 96 | margin: 0 1em 0 0.2em; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/client/components/repository/styles/Languages.less: -------------------------------------------------------------------------------- 1 | .languages { 2 | width: 22%; 3 | height: 100%; 4 | margin: 2em 4em; 5 | padding: 0.5em 1.5em; 6 | border-radius: 3px; 7 | border-style: solid; 8 | border-width: 1px; 9 | border-color: rgb(225, 228, 232); 10 | 11 | dt { 12 | font-weight: 600; 13 | margin-bottom: 0.5em; 14 | } 15 | 16 | dd { 17 | display: flex; 18 | justify-content: space-between; 19 | margin: 0.5em 0.5em 0.5em 1em; 20 | 21 | span.language { 22 | font-size: 3/4em; 23 | color: rgb(88, 96, 105); 24 | } 25 | 26 | span.count { 27 | font-size: 3/4em; 28 | color: rgb(88, 96, 105); 29 | font-weight: 600; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/client/components/repository/styles/List.less: -------------------------------------------------------------------------------- 1 | .list { 2 | width: 78%; 3 | } 4 | -------------------------------------------------------------------------------- /src/client/components/repository/styles/Main.less: -------------------------------------------------------------------------------- 1 | .repositories-main { 2 | width: 60%; 3 | margin: 0 auto; 4 | display: flex; 5 | } 6 | -------------------------------------------------------------------------------- /src/client/components/repository/styles/Tabs.less: -------------------------------------------------------------------------------- 1 | .tabs { 2 | height: 4.5em; 3 | background-color: white; 4 | 5 | border-bottom-style: solid; 6 | border-bottom-color: rgba(200, 200, 200, 0.6); 7 | border-bottom-width: 1px; 8 | 9 | .tabs-main { 10 | width: 60%; 11 | height: 100%; 12 | margin: 0 auto; 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | 17 | .tab { 18 | margin: 0 1.5em; 19 | 20 | span { 21 | color: #586069; 22 | background-color: rgba(27, 31, 35, 0.08); 23 | border-radius: 5/4em; 24 | font-size: 6/7em; 25 | font-weight: 600; 26 | padding: 1px 5px; 27 | margin-left: 1/2em; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/client/containers/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Navigator from '../components/common/Navigator'; 4 | import TextInput from '../components/common/TextInput'; 5 | import './styles/Home.less'; 6 | 7 | class Home extends React.Component { 8 | static contextTypes = { 9 | router: PropTypes.object, 10 | } 11 | 12 | constructor() { 13 | super(); 14 | this.state = { search: '' }; 15 | this.onChange = this.onChange.bind(this); 16 | this.onEnter = this.onEnter.bind(this); 17 | } 18 | 19 | onChange(event) { 20 | this.setState({ 21 | search: event.target.value, 22 | }); 23 | } 24 | 25 | onEnter() { 26 | const search = this.state.search; 27 | if (search) { 28 | this.context.router.history.push(`/repositories?q=${search}&sort=stars&order=desc`); 29 | } else { 30 | throw new Error('repository cannot be null'); 31 | } 32 | } 33 | 34 | render() { 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | Koa React Universal 42 | 43 | 44 | lightweight React-Koa2 universal boilerplate, only what is essential! 45 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | } 60 | 61 | export default Home; 62 | -------------------------------------------------------------------------------- /src/client/containers/Repository.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import _ from 'lodash'; 4 | import Navigator from '../components/common/Navigator'; 5 | import Tabs from '../components/repository/Tabs'; 6 | import Main from '../components/repository/Main'; 7 | import { parse } from '../../shared/utils/url'; 8 | import './styles/Repository.less'; 9 | 10 | class Repository extends React.Component { 11 | componentDidMount() { 12 | const { dispatch, repositories, load } = this.props; 13 | const url = window.location.href; 14 | const query = parse(url).query; 15 | if (typeof load === 'function' && !_.isEqual(query, repositories.query)) { 16 | load(dispatch, url); 17 | } 18 | } 19 | 20 | render() { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | const mapStateToProps = state => ({ repositories: state.repository.repositories }); 32 | 33 | export default connect(mapStateToProps)(Repository); 34 | -------------------------------------------------------------------------------- /src/client/containers/__tests__/Home.spec.js: -------------------------------------------------------------------------------- 1 | import Home from '../Home'; 2 | import { snapshot } from '../../../shared/test'; 3 | 4 | describe('components/containers/Home', () => { 5 | snapshot({ 6 | component: Home, 7 | name: 'Home', 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/client/containers/__tests__/Repository.spec.js: -------------------------------------------------------------------------------- 1 | import Repository from '../Repository'; 2 | import { snapshot } from '../../../shared/test'; 3 | 4 | describe('components/container/Repository', () => { 5 | snapshot({ 6 | component: Repository, 7 | name: 'Repository', 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/client/containers/__tests__/__snapshots__/Home.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/containers/Home Home should render correctly 1`] = ` 4 | 7 | 10 | 13 | 16 | 21 | 29 | 33 | 34 | 35 | 38 | 39 | 47 | 48 | 49 | 52 | 53 | Pull Requests 54 | 55 | 56 | Issues 57 | 58 | 59 | Marketplace 60 | 61 | 62 | Gist 63 | 64 | 65 | 66 | 67 | 70 | 73 | Koa React Universal 74 | 75 | 78 | lightweight React-Koa2 universal boilerplate, only what is essential! 79 | 80 | 83 | 84 | 92 | 93 | 94 | 95 | 96 | 97 | `; 98 | -------------------------------------------------------------------------------- /src/client/containers/__tests__/__snapshots__/Repository.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/container/Repository Repository should render correctly 1`] = ` 4 | 5 | 8 | 11 | 16 | 24 | 28 | 29 | 30 | 33 | 34 | 42 | 43 | 44 | 47 | 48 | Pull Requests 49 | 50 | 51 | Issues 52 | 53 | 54 | Marketplace 55 | 56 | 57 | Gist 58 | 59 | 60 | 61 | 62 | 65 | 68 | 71 | Repositories 72 | 73 | 48K 74 | 75 | 76 | 79 | Code 80 | 81 | 3M 82 | 83 | 84 | 87 | Commits 88 | 89 | 622K 90 | 91 | 92 | 95 | Issues 96 | 97 | 169K 98 | 99 | 100 | 103 | Wikis 104 | 105 | 10K 106 | 107 | 108 | 111 | Users 112 | 113 | 207 114 | 115 | 116 | 117 | 118 | 121 | 124 | 127 | 128 | 129 | Languages 130 | 131 | 132 | 135 | Javascript 136 | 137 | 140 | 37524 141 | 142 | 143 | 144 | 147 | CSS 148 | 149 | 152 | 2021 153 | 154 | 155 | 156 | 159 | Typescript 160 | 161 | 164 | 1838 165 | 166 | 167 | 168 | 171 | HTML 172 | 173 | 176 | 1192 177 | 178 | 179 | 180 | 183 | Vue 184 | 185 | 188 | 792 189 | 190 | 191 | 192 | 195 | Ruby 196 | 197 | 200 | 487 201 | 202 | 203 | 204 | 207 | PHP 208 | 209 | 212 | 207 213 | 214 | 215 | 216 | 219 | C# 220 | 221 | 224 | 133 225 | 226 | 227 | 228 | 231 | CoffeeScript 232 | 233 | 236 | 133 237 | 238 | 239 | 240 | 243 | Python 244 | 245 | 248 | 124 249 | 250 | 251 | 252 | 253 | 254 | 255 | `; 256 | -------------------------------------------------------------------------------- /src/client/containers/styles/Home.less: -------------------------------------------------------------------------------- 1 | @import "~themes/default.less"; 2 | 3 | .home { 4 | height: 100%; 5 | 6 | .code-lines { 7 | background: url(~assets/svg/codelines.svg), #2b3137; 8 | background-size: cover; 9 | background-position: center; 10 | } 11 | 12 | .main { 13 | width: 100%; 14 | height: 100%; 15 | position: relative; 16 | 17 | .center { 18 | position: absolute; 19 | left: 50%; 20 | top: 50%; 21 | transform: translate(-50%, -80%); 22 | 23 | .title { 24 | font-size: 5.5em; 25 | color: white; 26 | -webkit-font-smoothing: antialiased; 27 | font-family: Roboto, BlinkMacSystemFont, sans-serif; 28 | text-align: center; 29 | } 30 | 31 | .spec { 32 | margin-top: 0.5em; 33 | font-size: 2.5em; 34 | color: rgba(255, 255, 255, 0.6); 35 | -webkit-font-smoothing: antialiased; 36 | font-family: Roboto, BlinkMacSystemFont, sans-serif; 37 | text-align: center; 38 | } 39 | 40 | .search-area { 41 | margin-top: 4em; 42 | text-align: center; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/client/containers/styles/Repository.less: -------------------------------------------------------------------------------- 1 | @import "~themes/default.less"; -------------------------------------------------------------------------------- /src/client/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import configureStore from './models/store'; 6 | import Routes from './routes'; 7 | 8 | const store = configureStore(window.__STATE__); 9 | 10 | const render = (Component) => { 11 | ReactDOM.render(( 12 | 13 | 14 | 15 | 16 | 17 | ), document.getElementById('root')); 18 | }; 19 | 20 | if (module.hot) { 21 | module.hot.accept('./routes/index.jsx', () => { 22 | const Routes = require('./routes').default; 23 | render(Routes); 24 | }); 25 | } 26 | 27 | render(Routes); 28 | -------------------------------------------------------------------------------- /src/client/models/actions/__tests__/create.spec.js: -------------------------------------------------------------------------------- 1 | import create from '../create'; 2 | import configureStore from '../../store'; 3 | 4 | const store = configureStore(); 5 | const dispatch = store.dispatch; 6 | 7 | describe('models/actions/create', () => { 8 | it('should create an async action (promise)', () => { 9 | const action = create(async (dispatch) => {}); 10 | expect(action).toBeDefined(); 11 | expect(action()).toBeInstanceOf(Promise); 12 | }); 13 | 14 | it('should throw an error', () => { 15 | const error = 'an error!'; 16 | const action = create(async (dispatch) => { 17 | throw error; 18 | }); 19 | expect(action()).rejects.toMatch(error); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/client/models/actions/create.js: -------------------------------------------------------------------------------- 1 | 2 | // const isNode = (typeof process !== 'undefined') && 3 | // (typeof process.versions.node !== 'undefined'); 4 | 5 | /** 6 | * Action creator, 7 | * with centralized error-handling for redux-thunk actions. 8 | * @param {Function(dispatch)} action 9 | * @return {Function(dispatch)} wrapped up async action 10 | */ 11 | export default function create(action) { 12 | return async (dispatch) => { 13 | try { 14 | await action(dispatch); 15 | } catch (error) { 16 | throw error; 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/client/models/actions/repository.js: -------------------------------------------------------------------------------- 1 | import create from './create'; 2 | import API from '../../../shared/http/api'; 3 | 4 | export const listRepositories = ({ query }) => 5 | create(async (dispatch) => { 6 | dispatch({ 7 | type: 'setRepositories', 8 | payload: { 9 | sync: false, 10 | query, 11 | }, 12 | }); 13 | const result = await API.listRepositories({ query }); 14 | dispatch({ 15 | type: 'setRepositories', 16 | payload: { 17 | data: result.items, 18 | sync: true, 19 | total: result.total_count, 20 | query, 21 | }, 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/client/models/reducers/createReducer.js: -------------------------------------------------------------------------------- 1 | const createReducer = (initState, handlers) => { 2 | const reducer = (state = initState, action) => { 3 | const handler = handlers[action.type]; 4 | return handler ? handler(state, action) : state; 5 | }; 6 | return reducer; 7 | }; 8 | 9 | export default createReducer; 10 | -------------------------------------------------------------------------------- /src/client/models/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import repository from './repository'; 3 | 4 | const usecaseReducers = { 5 | repository, 6 | }; 7 | 8 | export default combineReducers({ ...usecaseReducers }); 9 | -------------------------------------------------------------------------------- /src/client/models/reducers/repository.js: -------------------------------------------------------------------------------- 1 | import createReducer from './createReducer'; 2 | 3 | const initialState = { 4 | repositories: { 5 | data: undefined, 6 | query: undefined, 7 | total: -1, 8 | sync: false, 9 | }, 10 | }; 11 | 12 | const handlers = { 13 | setRepositories: (state, { payload }) => ({ ...state, repositories: payload }), 14 | }; 15 | 16 | export default createReducer(initialState, handlers); 17 | -------------------------------------------------------------------------------- /src/client/models/store/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore, applyMiddleware, 3 | } from 'redux'; 4 | import thunkMiddleware from 'redux-thunk'; 5 | import reducers from '../reducers'; 6 | 7 | const createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore); 8 | 9 | const configureStore = initialState => 10 | createStoreWithMiddleware(reducers, initialState); 11 | 12 | export default configureStore; 13 | -------------------------------------------------------------------------------- /src/client/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | import universal from 'react-universal-component'; 4 | import RouteWithSubRoutes from '../components/common/RouteWithSubRoutes'; 5 | import { listRepositories } from '../models/actions/repository'; 6 | import { parse } from '../../shared/utils/url'; 7 | 8 | export const routes = [ 9 | { 10 | key: 'home', 11 | path: '/', 12 | exact: true, 13 | component: universal(import('../containers/Home')), 14 | }, { 15 | key: 'repositories', 16 | path: '/repositories', 17 | component: universal(import('../containers/Repository')), 18 | load: (dispatch, url) => { 19 | const query = parse(url).query; 20 | return dispatch(listRepositories({ query })); 21 | }, 22 | }, 23 | ]; 24 | 25 | export default () => ( 26 | 27 | { routes.map(route => ) } 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /src/development.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | require('colors'); 3 | const Koa = require('koa'); 4 | const webpack = require('webpack'); 5 | const { webpackServer, findCompiler } = require('koa-webpack-server'); 6 | const configs = require('../config/webpack.dev.config'); 7 | 8 | const app = new Koa(); 9 | const compilers = webpack(configs); 10 | const clientCompiler = findCompiler(compilers, 'client'); 11 | 12 | const options = { 13 | compilers, 14 | dev: { 15 | noInfo: false, 16 | quiet: true, 17 | serverSideRender: true, 18 | publicPath: clientCompiler.options.output.publicPath, 19 | }, 20 | }; 21 | 22 | console.log(`${'[SYS]'.rainbow} webpack building...`); 23 | 24 | // koa-webpack-server: https://github.com/kimjuny/koa-webpack-server 25 | webpackServer(app, options).then(({ middlewares }) => { 26 | const { logger, favicon, views, render } = middlewares; 27 | 28 | // koa2 hot middlewares: once any changes have made to these middlewares, 29 | // they will automatically hot patched, so you don't have to restart node. 30 | app.use(logger); 31 | app.use(favicon); 32 | app.use(views); 33 | app.use(render); 34 | 35 | app.listen(process.env.PORT, () => { 36 | console.log(`${'[SYS]'.rainbow} server started at port %s`, process.env.PORT); 37 | }); 38 | }).catch(() => { 39 | }); 40 | /* eslint-enable */ 41 | -------------------------------------------------------------------------------- /src/production.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, import/no-unresolved */ 2 | require('colors'); 3 | const webpack = require('webpack'); 4 | const Koa = require('koa'); 5 | const configs = require('../config/webpack.prod.config'); 6 | 7 | console.log(`${'[SYS]'.rainbow} webpack building...`); 8 | 9 | webpack(configs).run((err, stats) => { 10 | const app = new Koa(); 11 | 12 | // wire webpack stats for server render 13 | app.use(async (ctx, next) => { 14 | ctx.state.webpackStats = stats; 15 | await next(); 16 | }); 17 | 18 | const { 19 | logger, favicon, statics, views, render, 20 | } = require('../build/server/server'); 21 | 22 | // koa2 middlewares 23 | app.use(logger); 24 | app.use(favicon); 25 | app.use(statics); 26 | app.use(views); 27 | app.use(render); 28 | 29 | // start 30 | app.listen(process.env.PORT, () => { 31 | console.log(`${'[SYS]'.rainbow} server started at port ${process.env.PORT}`); 32 | }); 33 | }); 34 | /* eslint-enable */ 35 | -------------------------------------------------------------------------------- /src/server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "no-await-in-loop": 0, 6 | "no-shadow": 0, 7 | "import/no-extraneous-dependencies": 0, 8 | "import/prefer-default-export": 0, 9 | "no-underscore-dangle": 0, 10 | "global-require": 0, 11 | "no-console": 0 12 | }, 13 | "globals": { 14 | "__ROOT__": true, 15 | "document": true, 16 | "window": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/server/infrastructure/middlewares/favicon.js: -------------------------------------------------------------------------------- 1 | import favicon from 'koa-favicon'; 2 | 3 | export default favicon(`${__ROOT__}/src/assets/favicon.ico`); 4 | -------------------------------------------------------------------------------- /src/server/infrastructure/middlewares/index.js: -------------------------------------------------------------------------------- 1 | // this index file is the entry of webpack server-side config. 2 | // just export all your async koa2 middlewares here. 3 | 4 | export const logger = require('./logger').default; 5 | 6 | export const favicon = require('./favicon').default; 7 | 8 | export const statics = require('./statics')(`${__ROOT__}/build`, { prefix: '/build' }); 9 | 10 | export const views = require('./views').default; 11 | 12 | export const render = require('./render').default; 13 | -------------------------------------------------------------------------------- /src/server/infrastructure/middlewares/logger.js: -------------------------------------------------------------------------------- 1 | 2 | const logger = async (ctx, next) => { 3 | const now = new Date(); 4 | await next(); 5 | const ms = new Date() - now; 6 | console.log(`[${ctx.method}][${ms}ms] ${ctx.path}`); 7 | }; 8 | 9 | export default logger; 10 | -------------------------------------------------------------------------------- /src/server/infrastructure/middlewares/render.jsx: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | import React from 'react'; 3 | import { StaticRouter, matchPath } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import { renderToString } from 'react-dom/server'; 6 | import { flushChunkNames } from 'react-universal-component/server'; 7 | import webpackFlushChunks from 'webpack-flush-chunks'; 8 | import Routes, { routes } from '../../../client/routes'; 9 | import configureStore from '../../../client/models/store'; 10 | import commons from '../../../../config/commons'; 11 | 12 | const getClientStats = (stats) => { 13 | if (stats && Array.isArray(stats.stats)) { 14 | return stats.stats.find(node => 15 | node.compilation.name === commons.client); 16 | } else if (stats && stats.compilation.name === commons.client) { 17 | return stats; 18 | } 19 | return undefined; 20 | }; 21 | 22 | const match = async (ctx, routes, dispatch) => { 23 | for (let i = 0; i < routes.length; i += 1) { 24 | const route = routes[i]; 25 | if (matchPath(ctx.path, route)) { 26 | if (typeof route.load === 'function') { 27 | await route.load(dispatch, ctx.request.href); 28 | } 29 | if (route.routes && route.routes.length > 0) { 30 | await match(ctx, route.routes, dispatch); 31 | } 32 | return true; 33 | } 34 | } 35 | return false; 36 | }; 37 | 38 | const render = async (ctx) => { 39 | try { 40 | const store = configureStore(); 41 | 42 | if (await match(ctx, routes, store.dispatch)) { 43 | const content = renderToString( 44 | 45 | 49 | 50 | 51 | , 52 | ); 53 | 54 | const chunkNames = flushChunkNames(); 55 | const stats = getClientStats(ctx.state.webpackStats); 56 | const { js, styles, cssHash } = webpackFlushChunks(stats.toJson(), { chunkNames }); 57 | 58 | await ctx.render('200', { content, js, styles, cssHash, state: store.getState() }); 59 | } else { 60 | await ctx.render('404', { message: 'Page not found :-(' }); 61 | } 62 | } catch (error) { 63 | console.error(`${'[ERR]'.rainbow} SSR %s`, error.message); 64 | await ctx.render('500', { 65 | message: `message: ${error.message} 66 | errors: ${JSON.stringify(error.errors)}`, 67 | }); 68 | } 69 | }; 70 | 71 | export default render; 72 | -------------------------------------------------------------------------------- /src/server/infrastructure/middlewares/statics.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const debug = require('debug')('koa-static') 3 | const { resolve } = require('path') 4 | const assert = require('assert') 5 | const send = require('koa-send') 6 | 7 | /** 8 | * Expose `serve()`. 9 | */ 10 | module.exports = serve 11 | 12 | /** 13 | * Serve static files from `root`. 14 | * 15 | * @param {String} root 16 | * @param {Object} [opts] 17 | * @return {Function} 18 | * @api public 19 | */ 20 | function serve (root, opts) { 21 | opts = opts || {} 22 | 23 | assert(root, 'root directory is required to serve files') 24 | 25 | // options 26 | debug('static "%s" %j', root, opts) 27 | opts.root = resolve(root) 28 | if (opts.index !== false) opts.index = opts.index || 'index.html' 29 | 30 | if (!opts.defer) { 31 | return async function serve (ctx, next) { 32 | let done = false 33 | 34 | if (ctx.method === 'HEAD' || ctx.method === 'GET') { 35 | try { 36 | let path = ctx.path; 37 | if (opts.prefix && path.indexOf(opts.prefix) === 0) { 38 | path = path.slice(opts.prefix.length); 39 | } 40 | done = await send(ctx, path, opts); 41 | } catch (err) { 42 | if (err.status !== 404) { 43 | throw err 44 | } 45 | } 46 | } 47 | 48 | if (!done) { 49 | await next() 50 | } 51 | } 52 | } 53 | 54 | return async function serve (ctx, next) { 55 | await next() 56 | 57 | if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return 58 | // response is already handled 59 | if (ctx.body != null || ctx.status !== 404) return // eslint-disable-line 60 | 61 | try { 62 | await send(ctx, ctx.path, opts) 63 | } catch (err) { 64 | if (err.status !== 404) { 65 | throw err 66 | } 67 | } 68 | } 69 | } 70 | /* eslint-enable */ 71 | -------------------------------------------------------------------------------- /src/server/infrastructure/middlewares/views.js: -------------------------------------------------------------------------------- 1 | import views from 'koa-views'; 2 | 3 | export default views( 4 | `${__ROOT__}/src/server/infrastructure/templates`, 5 | { map: { html: 'ejs' }, extension: 'ejs' }, 6 | ); 7 | -------------------------------------------------------------------------------- /src/server/infrastructure/templates/200.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | koa-react-universal 7 | <%- styles %> 8 | 9 | 10 | <%- content %> 11 | 18 | <%- cssHash %> 19 | <%- js %> 20 | 21 | -------------------------------------------------------------------------------- /src/server/infrastructure/templates/404.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | koa-react-universal 7 | 8 | 9 | 10 | <%- message %> 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/server/infrastructure/templates/500.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | koa-react-universal 7 | 8 | 9 | 10 | <%- message %> 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/shared/base/exceptions/RequestError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * network error 3 | */ 4 | function RequestError(message, errors) { 5 | this.name = message; 6 | this.errors = errors; 7 | this.message = message; 8 | } 9 | 10 | RequestError.proptype = Object.create(Error.prototype); 11 | RequestError.proptype.constructor = RequestError; 12 | 13 | export default RequestError; 14 | -------------------------------------------------------------------------------- /src/shared/http/api.js: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | import { objectToQueryString } from '../utils/url'; 3 | 4 | const base = 'https://api.github.com'; 5 | 6 | /** 7 | * Github RESTful API v3: 8 | * @link https://developer.github.com/v3/ 9 | */ 10 | class API { 11 | static listRepositories({ query }) { 12 | return request(`${base}/search/repositories${objectToQueryString(query)}`); 13 | } 14 | } 15 | 16 | export default API; 17 | -------------------------------------------------------------------------------- /src/shared/test/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension, no-undef */ 2 | import React from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import renderer from 'react-test-renderer'; 6 | import configureStore from '../../client/models/store'; 7 | 8 | export function snapshot(options) { 9 | const { name, props = {}, store } = options; 10 | it(`${name} should render correctly`, () => { 11 | const wrapper = ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | const tree = renderer.create(wrapper).toJSON(); 19 | expect(tree).toMatchSnapshot(); 20 | }); 21 | } 22 | /* eslint-enable */ 23 | -------------------------------------------------------------------------------- /src/shared/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import RequestError from '../base/exceptions/RequestError'; 3 | 4 | const postProcess = response => response.data; 5 | 6 | export default function request(url, options) { 7 | const headers = { 8 | Accept: 'application/vnd.github.mercy-preview+json', 9 | }; 10 | 11 | return axios({ url, timeout: 8000, headers, ...options }) 12 | .then(postProcess) 13 | .catch((error) => { 14 | const response = error.response; 15 | if (response && response.data.message === 'Validation Failed') { 16 | throw new RequestError('Validation Failed', error.response.data.errors); 17 | } else { 18 | throw error; 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/shared/utils/url.js: -------------------------------------------------------------------------------- 1 | import parser from 'url-parse'; 2 | 3 | /** 4 | * parse URL 5 | * @param {*} url 6 | * @return parsed result 7 | * { 8 | * auth: 9 | * hash: 10 | * host: localhost:3000 11 | * hostname: localhost 12 | * href: http://localhost:3000/products?offset=0&limit=3000 13 | * origin: http://localhost:3000 14 | * password: 15 | * pathname: /products 16 | * port: 3000 17 | * protocol: http 18 | * query: { 19 | * offset: 0, 20 | * limit: 10, 21 | * }, 22 | * slashes: true, 23 | * username: 24 | * } 25 | * for mor detail please refer to: https://github.com/unshiftio/url-parse 26 | */ 27 | export const parse = (url) => { 28 | const parsed = parser(url); 29 | if (parsed.query.length > 0) { 30 | const qString = parsed.query.slice(1); 31 | const pairArray = qString.split('&'); 32 | const query = {}; 33 | pairArray.forEach((value) => { 34 | const node = value.split('='); 35 | query[node[0]] = node[1]; 36 | }); 37 | parsed.query = query; 38 | } 39 | return parsed; 40 | }; 41 | 42 | /** 43 | * parse query object to query string 44 | * @param {*} obj 45 | * @return String { hello: 'world', simple: 'example' } => '?hello=world&simple=example' 46 | */ 47 | export const objectToQueryString = (obj) => { 48 | const queryArray = Object.keys(obj).map(key => `${key}=${obj[key]}`); 49 | return `?${queryArray.join('&')}`; 50 | }; 51 | -------------------------------------------------------------------------------- /src/themes/default.less: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | padding: 0; 3 | margin: 0; 4 | height: 100%; 5 | width: 100%; 6 | 7 | font-size: 14px; 8 | } 9 | 10 | .text-normal { 11 | color: #586069; 12 | } 13 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (wallaby) => { 3 | return { 4 | files: [ 5 | { pattern: 'src/**/*.jsx', load: true }, 6 | { pattern: 'src/**/__tests__/*.spec.js', ignore: true }, 7 | { pattern: 'src/**/*.js', load: true }, 8 | { pattern: 'src/**/*.less', ignore: true }, 9 | { pattern: 'jest.config.js', load: true }, 10 | { pattern: 'config/*.js', ignore: true }, 11 | ], 12 | tests: [ 13 | 'src/**/__tests__/*.spec.js', 14 | ], 15 | env: { 16 | type: 'node', 17 | runner: 'node', 18 | }, 19 | testFramework: 'jest', 20 | 21 | compilers: { 22 | '**/*.js': wallaby.compilers.babel({ babelrc: true }), 23 | '**/*.jsx': wallaby.compilers.babel({ babelrc: true }), 24 | }, 25 | 26 | setup: (wallaby) => { 27 | wallaby.testFramework.configure(require('./jest.config.js')); 28 | }, 29 | }; 30 | }; 31 | --------------------------------------------------------------------------------
{ repository.language }
29 | { repository.stargazers_count } 30 |
38 | language 39 |
58 | 100 59 |
45 | language 46 |
65 | 100 66 |