├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .travis.yml ├── README.md ├── __fixtures__ └── store.js ├── __mocks__ └── fileMock.js ├── __tests__ ├── common │ └── components │ │ └── Example.spec.js ├── redux │ ├── modules │ │ └── example.js │ └── sagas │ │ └── exampleSagas.spec.js └── setup │ └── jestTestSetup.js ├── bin └── commands.js ├── config ├── default.json ├── development.json ├── production.json └── stage.json ├── package.json ├── src ├── assets │ ├── fonts │ │ ├── YourFavoriteFont.eot │ │ └── YourFavoriteFont.otf │ └── template │ │ └── _default.html ├── js │ ├── README.md │ ├── Root.jsx │ ├── app-history.js │ ├── common │ │ ├── api │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ └── module │ │ │ │ └── example.js │ │ ├── components │ │ │ ├── Example │ │ │ │ ├── Example.css │ │ │ │ ├── Example.jsx │ │ │ │ ├── ExampleWithError.jsx │ │ │ │ └── index.js │ │ │ ├── Header │ │ │ │ ├── Header.css │ │ │ │ ├── Header.jsx │ │ │ │ └── index.js │ │ │ ├── LazyLoading │ │ │ │ ├── Loading.css │ │ │ │ ├── Loading.jsx │ │ │ │ └── index.js │ │ │ └── Utilities │ │ │ │ ├── ErrorBoundary.jsx │ │ │ │ ├── I18NProvider.jsx │ │ │ │ └── index.js │ │ ├── translations │ │ │ └── en-US.json │ │ └── types │ │ │ └── example.js │ ├── main.jsx │ ├── redux │ │ ├── configureStore.js │ │ ├── modules │ │ │ ├── example.js │ │ │ └── index.js │ │ ├── rootReducers.js │ │ ├── sagas │ │ │ ├── exampleSaga.js │ │ │ └── index.js │ │ └── selectors │ │ │ └── exampleSelector.js │ ├── routes.jsx │ └── views │ │ ├── README.md │ │ └── example │ │ ├── View.jsx │ │ └── index.js └── style │ ├── configs │ ├── color.css │ └── font-faces.css │ └── index.css ├── webpack.config.build.babel.js ├── webpack.config.common.js └── webpack.config.dev-server.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": ["last 2 versions", "ie >= 10"] 6 | } 7 | }], 8 | "react", 9 | "stage-0", 10 | "flow" 11 | ], 12 | "env": { 13 | "development": { 14 | "presets": ["react-hmre"] 15 | }, 16 | "production": { 17 | "presets": ["react-optimize"] 18 | }, 19 | }, 20 | "plugins": [ 21 | ["typecheck", { 22 | "disable": { 23 | "production": true 24 | } 25 | }] 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | charset = utf-8 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /docroot 2 | /node_modules/ 3 | npm-debug.log 4 | /__tests__ 5 | /blueprints 6 | preprocessor.js 7 | webpack.config.babel.js 8 | webpack.config.common.js 9 | webpack.config.dev.js 10 | webpack.config.prod.js 11 | /vendor/ 12 | /bower_components/ 13 | /src/assets 14 | /src/style 15 | /bin 16 | /dist 17 | /dist-server 18 | /config/webpack/dist 19 | *.md 20 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "globals": { 8 | "__CONFIG__": true, 9 | "__ENV__": true 10 | }, 11 | "extends": ["airbnb"], 12 | "plugins": ["flowtype", "react"], 13 | "parser": "babel-eslint", 14 | "parserOptions": { 15 | "ecmaFeatures": { 16 | // Enabled object spread 17 | "experimentalObjectRestSpread": true 18 | } 19 | }, 20 | // Rule changes from Airbnb's eslint config 21 | "rules": { 22 | "import/no-unresolved": 0, 23 | "new-cap": 0, 24 | "semi": 0, 25 | "global-require": 0, 26 | "no-underscore-dangle": 0, 27 | "arrow-body-style": 0, 28 | "no-console": 0, 29 | "react/forbid-prop-types": 0, 30 | "jsx-a11y/href-no-hash": 0, 31 | "import/prefer-default-export": 0, 32 | "react/prop-types": 0, 33 | "react/jsx-filename-extension": 0, 34 | "comma-dangle": [2, "always-multiline"], 35 | "arrow-parens": ["warn", "always"], 36 | "jsx-a11y/anchor-is-valid": [ "error", { 37 | "components": [ "Link" ], 38 | "specialLink": [ "to", "hrefLeft", "hrefRight" ], 39 | "aspects": [ "noHref", "invalidHref", "preferButton" ] 40 | }], 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /bower_components/ 3 | /node_modules/ 4 | /.idea 5 | /docroot/ 6 | /build/ 7 | .DS_Store 8 | npm-debug.log 9 | .eslintcache 10 | /.env 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | script: npm run test 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Build Status 2 | [![linux build](https://api.travis-ci.org/iroy2000/react-redux-boilerplate.svg?branch=master)](https://travis-ci.org/iroy2000/react-redux-boilerplate) 3 | [![Dependency Status][david_img]][david_site] 4 | [![Join the chat at https://gitter.im/iroy2000-react-redux-boilerplate/Lobby](https://badges.gitter.im/iroy2000-react-redux-boilerplate/Lobby.svg)](https://gitter.im/iroy2000-react-redux-boilerplate/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | ### TL;DR 7 | 8 | Before you dive into anything, see for yourself how easy you can setup a full workflow framework for your `development` and `deployment` for your real world project. 9 | 10 | Step 1: Clone this repo 11 | ``` 12 | git clone https://github.com/iroy2000/react-redux-boilerplate.git 13 | cd react-redux-boilerplate 14 | ``` 15 | 16 | Step 2: Install 17 | 18 | ``` 19 | npm install 20 | ``` 21 | 22 | Step 3: Start 23 | 24 | ``` 25 | npm run dev 26 | ``` 27 | 28 | And Done, as easy as 123!! 29 | 30 | 31 | ### Preface 32 | 33 | React Redux Boilerplate is a full fledged __PRODUCTION READY__ workflow boilerplate for building complex React / Redux application. 34 | 35 | The technologies used in this workflow boilerplate shows case a `subset of technologies` we are currently using within our team ( __we put this project in github mainly as recruiting purpose for our team__ ), and we are trying to give our candidates confidence that by working with us, they will be using decent technologies as well as staying relevant to the industry. And that is what our team strongly believed in - __technology innovation and promoting developers' relevancy in the industry__. 36 | 37 | If you are interested in working with us, feel free to send a message to [iroy2000](https://www.linkedin.com/in/iroy2000). 38 | 39 | __Note:__ This boilerplate is not to show case how to code in React / Redux, it is a boilerplate that hook everything up for you to develop a complex React / Redux project. And if you just want to learn React, we don't have complicated code example here and this boilerplate might be too complicated for your need. 40 | 41 | ![React Redux Workflow Boilerplate Logo](http://res.cloudinary.com/search-engine/image/upload/v1501696716/logo-rrb-002_zxvml0.png) 42 | 43 | # React Redux Boilerplate 44 | 45 | `React Redux Boilerplate` is a workflow boilerplate that make life easier for developers by providing a virtual development environment and production ready build process framework out of the box. 46 | 47 | `React Redux Boilerplate` is for developing React client side application. So, if you are looking for: 48 | 49 | * `Isomorphic (Universal) support`, feel free to add server side support to it, or you can use something like [Next.js](https://github.com/zeit/next.js/), [react-server](https://github.com/redfin/react-server) or [electrode](http://www.electrode.io/) 50 | 51 | 52 | ### Features / Benefits 53 | 54 | Features 55 | 56 | * React 16.6 57 | * Redux 58 | * Saga 59 | * ES6 / ES7 60 | * ImmutableJS 61 | * PreCSS ( supports SASS-like markup in your CSS ) 62 | * PostCSS ( with CSS modules activated by default ) 63 | * Webpack 4 64 | * Reselect 65 | * i18n / i10n supports ( react-intl ) 66 | * Lazy Loading component supports 67 | * Type Checking with Babel Type Check ( Flow syntax ) 68 | * ESLint for syntax check 69 | * Jest and Enzyme for Unit testing 70 | 71 | Workflow 72 | 73 | * Development 74 | * Hot Module Reload during development 75 | * Built-in lightweight config system 76 | * Built-in fancy cli dashboard for reporting run time compile status 77 | * Built-in support for multiple device concurrent debugging 78 | * Build / Production 79 | * Production bundle analyzing capability 80 | * CSS / HTML / JS minification / Image optimization when built 81 | * JS code duplication removal during built ( tree shaking capability ) 82 | * Deployment 83 | * Built-in git commit hook, helpful for CI/CD process 84 | * Built-in process to deploy files directly to S3 ( optional ) 85 | * Productivity 86 | * Highly configurable build and workflow system ( webpack ) 87 | * Minimal setup time and allow you to invest into things that matters 88 | * Everything automatic, you just care about development, nothing else \o/ Yeah ?! 89 | 90 | If you are interested, please read the `package.json` for all installed modules and plugins. 91 | 92 | ## Table of Contents 93 | 94 | Basic 95 | 1. [Installation](#installation) 96 | 1. [Initialize your project](#initialize-your-project) 97 | 1. [Suggested Workflow](#suggested-workflow) 98 | 1. [Folder Structure](#folder-structure) 99 | 1. [Production Readiness](#production-readiness) 100 | 1. [Configuration](#configuration) 101 | 1. [Port Configuration](#port-configuration) 102 | 1. [Installing Dependencies](#installing-dependencies) 103 | 104 | Advanced 105 | 1. [Lazy Loading Component](#lazy-loading-component) 106 | 1. [Writing Unit Test](#writing-unit-test) 107 | 1. [Configure git commit hook](#configure-git-commit-hook) 108 | 1. [Multiple Device Concurrent Debugging](#multiple-device-concurrent-debugging) 109 | 1. [Developing Template](#developing-template) 110 | 1. [Production Optimization and Bundle Analysis](#production-optimization-and-bundle-analysis) 111 | 1. [Integration Note](#integration-note) 112 | 1. [QA](#qa) 113 | 114 | Other 115 | 1. [Knowledge Base Reading](#knowledge-base-reading) 116 | 1. [How to Contribute](#how-to-contribute) 117 | 1. [Updates](#updates) 118 | 119 | # Basic 120 | 121 | ## Installation 122 | 123 | 124 | ### Prerequisite 125 | 126 | You need to have Node.js installed. 127 | 128 | [Instruction for installing NodeJS in Mac](http://lmgtfy.com/?q=install+nodejs+mac) 129 | 130 | [Instruction for installing NodeJS in Window](http://lmgtfy.com/?q=install+nodejs+window) 131 | 132 | ### Post Installation 133 | 134 | If you would like to have Redux debug capabilities, you can download this Chrome extension [Redux DevTool](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en) 135 | 136 | ![Redux DevTool](https://www.dropbox.com/s/wni425e3d4xiy85/redux-devtool.png?raw=1) 137 | 138 | 139 | ## Initialize your project 140 | 141 | Now run the following commands in your terminal 142 | 143 | **NOTE: You only need to run this once!** 144 | 145 | ```sh 146 | $ npm install # This will install the necessary packages to use the app 147 | ``` 148 | 149 | **That's it!** 150 | 151 | 152 | ### To run the app in Development Mode 153 | 154 | ```sh 155 | $ npm run dev 156 | ``` 157 | 158 | Wait about 30 seconds for your development environment to initialize. 159 | 160 | When it finishes, open your browser and go to `http://localhost:8080/` 161 | 162 | If you see the landing page, it means you have set up everything successfully. 163 | 164 | 165 | ### List of NPM Commands 166 | 167 | 168 | ```sh 169 | $ npm run dev # build and watch, but javascript not minified 170 | $ npm run build # build a minified production version 171 | $ npm run build:s3 # build a minified production version, deploy it to S3 as a static app 172 | $ npm run lint # linting using ESLint 173 | $ npm run test # run test using Jest 174 | $ npm run clean # it runs before each build, so you don't need to 175 | ``` 176 | 177 | 178 | ## Suggested Workflow 179 | 180 | 181 | After you check out the repo, I will usually do the following : 182 | 183 | 0. Go to your project root in your host machine ( e.g. your Mac ) 184 | 1. Run `npm run dev` in your terminal ( wait until the dashboard show complete status ) 185 | 2. Go to your browser and go to `localhost:8080` 186 | 3. Make code changes 187 | 4. If there are compilation errors, you will see it in the terminal dashboard 188 | 5. Watch your code changes reflect on browser without refreshing 189 | 6. Repeat your development steps 190 | 191 | That's very easy, isn't it? 192 | 193 | ## Folder Structure 194 | 195 | The entry point of your application is `src/js/routes`, it's basically a mapping between your `views` to a `route`. 196 | 197 | All your javascript code lives in folder `src/js` 198 | 199 | ``` 200 | -- src/ 201 | -- js/ 202 | -- common/ 203 | -- api/ --> all api requests 204 | -- components/ --> all share components 205 | -- types/ --> all flow types 206 | -- redux/ 207 | -- modules/ --> all redux code 208 | -- saga/ --> all redux-saga code 209 | -- selectors/ --> all reselect code 210 | -- utility/ --> all non JSX utility 211 | -- views/ --> all JSX code hook up with Route ( HoC ) or page specific components 212 | -- style/ --> all global styles, layout, config 213 | -- assets/ --> all static assets ( image, fonts ... etc ) 214 | -- template/ --> you probably won't touch this unless you want to create new template 215 | 216 | ``` 217 | 218 | * For `config/` and `bin/` folder, it is covered at [Configuration](#configuration) section 219 | 220 | * For `__tests__/` folder, it is covered at [Writing Unit Test](#writing-unit-test) section 221 | 222 | * For our Redux coding style, we are using [Ducks](https://github.com/erikras/ducks-modular-redux) standards 223 | 224 | * For how to write code in ES6 /React / Redux / POSTCSS ... etc, please feel free to look at our simple example code. And if you have question or want to study future, please checkout the [Knowledge Base Reading](#knowledge-base-reading) section, which covers everything we used in this boilerplate. 225 | 226 | **NOTE: When you import resources, please make sure you have the right path** 227 | 228 | 229 | ## Production Readiness 230 | 231 | React Redux Boilerplate supports production preview, which means that you can run the production build job and see how it looks like in production. 232 | 233 | 1. Run `npm run build` and wait until it is done 234 | 2. Go to the project `docroot`, you will see a `index.html` (template is customizable, please read `Developing Template` section) 235 | 3. Serve the build directory, for example like so: 236 | 237 | ```bash 238 | npm i -g http-server 239 | cd docroot 240 | http-server 241 | ``` 242 | By default http-server will serve your production build at port 8080. Docs are [here](https://www.npmjs.com/package/http-server). 243 | 244 | 4. Navigate to [that address](http://localhost:8080) to see your build. 245 | 246 | That's very easy, isn't it? 247 | 248 | ### Difference between `npm run dev` v.s. `npm run build` 249 | 250 | `npn run dev` is best to do JS / CSS only changes, and it comes with live reload functionality 251 | 252 | `npm run build` is for testing what happen if your frontend assets are optimized ( production level code ) 253 | 254 | Please let me know if you have better work flow suggestion!! 255 | 256 | ## Configuration 257 | React Redux Boilerplate has two configuration strategies, one is for normal configuration, the other one is for sensitive information that you don't want others to know. 258 | 259 | ### Configuring application 260 | 261 | If you look at folder `config`, there are four files 262 | 263 | `default.json` - all default configuration 264 | `development.json` - when you run `npm run dev`, it will pull configuration from that file 265 | `release.json` - when you run `npm run build:release`, it will use this configuration 266 | `production.json` - when you run `npm run build`, it will use this configuration 267 | 268 | We are using [node-config](https://github.com/lorenwest/node-config), they have a pretty easy to understand documentation. 269 | 270 | 271 | And in your config file ( json config file ), whatever you put inside the `app`, it will be injected into the client application and you can access to your `app` config data by using `__CONFIG__` variables. 272 | 273 | Let's say you have a config like the following 274 | 275 | ``` 276 | { 277 | "app": { 278 | "apiURL": "http://foo.bar/" 279 | } 280 | } 281 | 282 | ``` 283 | 284 | In your React application, you can access this variables 285 | 286 | ``` 287 | __CONFIG__.apiURL 288 | 289 | ``` 290 | 291 | __Note:__ If you want to add new npm target ( e.g. `npm run build:stage` ), you need to do the following :- 292 | 293 | 1. Add a `stage.json` file inside `config` folder 294 | 2. Add `npm run build:stage` ( or similar ) at `package.json` scripts section 295 | 3. Add actual command mapping at `bin/commands.js` 296 | 297 | ### Configuring secret key/value pair 298 | 299 | There are times you may want to put in `secret information` you don't want to check into the source code. In this boilerplate, you just need to create a file called `.env` in your `PROJECT_ROOT`, and you can put your secrets there ( we have put that into `.gitignore` just in case ). 300 | 301 | Specifically for deployment to S3, there are two options for providing your secrets: 302 | 1. In ~/.aws/credentials, configure a block like so, for example for a profile called "default": 303 | 304 | ```bash 305 | [default] 306 | AWS_ACCESS_KEY_ID=XXXXXXXX # replace with your key 307 | AWS_SECRET_ACCESS_KEY=XXXXXXX # replace with your secret 308 | ``` 309 | 310 | 2. You can provide the same values in a `.env` file within your project: 311 | 312 | ```bash 313 | AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY 314 | AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_KEY 315 | ``` 316 | 317 | If you are using the AWS CLI, chances are you already have an `~/.aws/credentials` file, so you may find option 1 simpler locally. Option 2 may be better when using a build server like Jenkins. 318 | 319 | Finally, no matter which of the above two options you choose, you will ALSO need to provide these additional values in your `.env` file, or otherwise set them as environment variables when you build: 320 | 321 | ```bash 322 | AWS_BUCKET=YOUR_AWS_BUCKET # REQUIRED for S3 deploy 323 | AWS_CDN_URL=YOUR_AWS_CDN_URL # OPTIONAL for S3 deploy 324 | ``` 325 | 326 | And your in node application or webpack config, those key/value pairs will be injected into `process.env` ( e.g. `process.env.AWS_ACCESS_KEY_ID` ). 327 | 328 | __Note__: Using the `.env` file is optional, it meant to keep secret and inject information into environment variables, if you are using Jenkins or a like type of tool, you can inject environment variables there. 329 | 330 | However, with `.env`, you can create a ready to use list of environment variables for your different environment. You can even have another service to generate the `.env` file before building the project, but in terms of how to achieve that, it is out of scope of this documentation. 331 | 332 | __Just remember__, `.env` file is supposed to keep your secret, and prevent you from saving sensitive secrets into your source code repository \0/ !! **DO NOT** check `.env` into your source repo !! 333 | 334 | We are using [dotenv](https://github.com/motdotla/dotenv) for the `.env` feature, they have pretty good documentation. 335 | 336 | ## Port Configuration 337 | There will be a chance that you will need your port to be other than `8080`. For example, your local backend service might already take `8080`; Or you want to run multiple project, one on `8080` and one on `80801`. 338 | 339 | If you are running one project that needs a different port, you can just modify one place 340 | 1) `default.json` --> `port` section. 341 | 342 | But if you want to run multiple projects at the same time, you will need to configure ports in two places 343 | 1) `default.json` --> `port` 344 | 2) Dashboard port --> `package.json` --> `dev` ( default dashboard port is `9901` ) 345 | 346 | ## Installing Dependencies 347 | 348 | We are using `npm` in this project, so if you would like to install a dependencies, for example, D3, you can do something like the following :- 349 | 350 | `npm i --save d3` 351 | 352 | # Advanced 353 | 354 | ## Lazy Loading Component 355 | 356 | This boilerplate supports `Lazy Loading` and `Component Centric Splitting` out of the box by integrating `react-loadable`. 357 | 358 | In short, it will drastically reduce your initial load time by lazy loading components until the component is needed. And you can now do tricks like `above the fold` and `load component on demand` very easily with this capabilities. 359 | 360 | In order to take advantage of this capabilities 361 | 362 | You first need to import: 363 | 364 | ``` 365 | // Currently it lives inside common component, mostly as example 366 | // It is a react-loadable wrapper 367 | import LazyLoading from '../../common/components/LazyLoading' 368 | ``` 369 | 370 | And you now can lazy load anything like the following: 371 | 372 | ``` 373 | const LazyExample = LazyLoading(() => import('YOUR_PATH_TO/components/YourComponent')) 374 | ``` 375 | Can it be easier ?? 376 | 377 | Please look at the example in this repo and `react-loadable` to get more idea. 378 | 379 | ## Writing Unit Test 380 | 381 | We are using Jest and Enzyme for unit testing, please refer to the Knowledge Base section below for more information. 382 | 383 | In order to add unit test, the current setup requires you to put your test under `__tests__` directory, and suffix the filename with `spec` or `test`. 384 | 385 | For example, `MyFancyComponent.test.js` or `whatever_folder/AnotherComponent.spec.js`. 386 | 387 | We also have two folders `__fixtures` and `__mocks__` in the repo, those are just handy example folders for you to put different type of test related files separately, these two folders are safe to delete if you don't need them. 388 | 389 | ## Configure git commit hook 390 | 391 | We are using `husky` for preventing developers check in bad commits. Please go to package.json and look for `husky` the default settings. `husky` supports any git hooks and you can configure it to fit your needs. 392 | 393 | ## Multiple Device Concurrent Debugging 394 | 395 | React Redux Boilerpalate has built in support for multiple device concurrent access by entering the ip address provide. 396 | 397 | If you look at the dashboard, you should see some messages like below ( Note: your ip will be different ) 398 | 399 | ![Logo](http://res.cloudinary.com/search-engine/image/upload/v1501353275/share_screen_g3ofe1.png) 400 | 401 | For example, you can enter the address into multiple real devices and you can test those devices all at the same time, like the following :- 402 | 403 | ![Logo](http://res.cloudinary.com/search-engine/image/upload/v1501353811/multiple_screen_d2b7fg.png) 404 | 405 | ## Developing Template 406 | 407 | The `docroot/index.html` is a generated artifact. If look at our sample template at `src/assets/template/_default.html`, the `docroot/index.html` is generated from that file. 408 | 409 | We are using [HTML Webpack Plugin](https://github.com/ampedandwired/html-webpack-plugin) to generate the `docroot/index.html`. 410 | 411 | * If you are developing a single page application, you probably can reuse that file or customize it. 412 | * If you are building multiple pages application, please read the [HTML Webpack Plugin](https://github.com/ampedandwired/html-webpack-plugin) documentation for template development and how to configure it. 413 | 414 | **[Note]** - Most of the use case of the html template system so far is for testing purpose for your dev and build enviornment before you start integration. If all you need is a static template, you are good; but if your application requires a server side processing, you will need to integrate the artifacts with the language of your choice. Please read `Diagrams` section for how to integrate with other server side language. 415 | 416 | ## Production Optimization and Bundle Analysis 417 | 418 | For optimization, you can tweak the `config` under the following. This optimization covers basic use case, feel free to make modification to fit your needs. 419 | 420 | ``` 421 | "optimization": { 422 | "commonMinCount": 3, // how many times a module is used before we consider it a common module ? 423 | "cssExclusion": true, // do you want to exclude css from this optimization ? 424 | ... 425 | }, 426 | ``` 427 | 428 | This boilerplate shipped with `Production Bundle Analysis` tool which is `turn on` by default and will give you insight to optimize the production assets. 429 | 430 | For example, when you run `npm run build` with `"analyzeMode": true`, you should see something like the following at the end of your build. 431 | 432 | The tool is to help you understand what your bundle looks like and what is included in your bundle. 433 | 434 | ![Production Bundle Analysis](https://www.dropbox.com/s/zun4n4tgp059neh/bundle-analysis.png?raw=1) 435 | 436 | You can turn `on` and `off` the analysis feature 437 | 438 | ``` 439 | "optimization": { 440 | ... 441 | "analyzeMode": true, // changing to false will turn it off 442 | "analyze": { 443 | "port": 8888 // it will open localhost:8888 and show your bundle analysis 444 | } 445 | }, 446 | ``` 447 | 448 | ## Integration Note 449 | 450 | 451 | ### How to integrate with other server side framework ? 452 | 453 | When you run `npm run build`, it will generate a meta data file `assets.json` with the list of generated frontend artifacts. Think about that as a contract / interface for your server side framework. 454 | 455 | And your framework just need to consume that meta information as integration point. 456 | 457 | And this boilerplate has a process integrated to upload artifacts ( assets.json and generated client ifacts ) to S3 / cloudfront. One of the integration path is the `consumer` can query against s3 for the assets.json, and use the information in assets.json to get the artifacts which is uploaded to S3 / cdn. 458 | 459 | 460 | ## QA 461 | 462 | 463 | * __How to activate S3 support ?__ 464 | * S3 upload is optional here 465 | 1. Make sure you have provided your AWS credentials to the project (secret and access key). Please read [Configuration](#configuration) section for more information. 466 | 2. Use the `npm run build:s3` script to build and deploy. 467 | 468 | * __What is our standard to control our npm module dependencies ?__ 469 | * We are using `^version`, it means "Compatible with version". The reason we are using `^version` is simply we want the ability for us to roll back to previous working version together with the source code. 470 | 471 | * __How to add javascript unit test ?__ 472 | * All React JS test are under \__tests__ directory and this tool will find all the test, you don't need to do anything besides putting your test in, but please use a structure that mimic your source location that you are testing, or it will create confusion. 473 | 474 | * __What is B.E.M style ?__ 475 | * B.E.M is short for `Block, Element, Modifier` and is a naming convention for classes in HTML and CSS. Its goal is to help developers better understand the relationship between the HTML and CSS and make our code base more maintainable. Please read the links below for getting deeper insight of it. 476 | 477 | # Other 478 | 479 | ## Knowledge Base Reading 480 | 481 | ### ES6 482 | 483 | * [ES6 for Humans](https://github.com/metagrover/ES6-for-humans) 484 | * [ES6 Compatible Table](http://kangax.github.io/compat-table/es6/) 485 | * [ECMAScript Specification](https://www.ecma-international.org/ecma-262/) 486 | 487 | ### React 488 | 489 | * [Thinking in React](https://facebook.github.io/react/docs/thinking-in-react.html) 490 | * [React 16.4+ life cycle methods](https://blog.bitsrc.io/understanding-react-v16-4-new-component-lifecycle-methods-fa7b224efd7d) 491 | * [Synchronization of props with state is an anti-pattern](https://github.com/JedWatson/react-select/issues/70) 492 | * [You Probably Don't Need Derived State](https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html) 493 | * [How Virtual-DOM and diffing works in React](https://medium.com/@gethylgeorge/how-virtual-dom-and-diffing-works-in-react-6fc805f9f84e) 494 | 495 | ### Redux 496 | 497 | * [Redux](http://redux.js.org/) 498 | * [You might not need Redux](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367) 499 | * [Redux without profanity](https://www.gitbook.com/book/tonyhb/redux-without-profanity/details) 500 | * [Learned from reading the Redux source code](https://medium.freecodecamp.com/what-i-learned-from-reading-the-redux-source-code-836793a48768?gi=4bdf3c9f3396#.rjwu6x9sc) 501 | * [Redux Saga](https://redux-saga.js.org/) 502 | * [Usage of Reselect in a React-Redux Application](https://dashbouquet.com/blog/frontend-development/usage-of-reselect-in-a-react-redux-application) 503 | 504 | ### Webpack 505 | 506 | * [Webpack how-to](https://github.com/petehunt/webpack-howto) 507 | * [Webpack - The Confusing Part](https://medium.com/@rajaraodv/webpack-the-confusing-parts-58712f8fcad9) 508 | * [Lazy Loading and Code Split for React Route](https://github.com/webpack/react-proxy-loader) 509 | * [Lazy Loading and Code Split for Components](https://github.com/jamiebuilds/react-loadable) 510 | 511 | 512 | ### Relevant Knowledge 513 | 514 | * [ImmutableJS for beginners](https://github.com/iroy2000/immutablejs-for-beginners) 515 | * [Learning PostCSS](https://github.com/postcss/postcss) 516 | * [Jest = Awesome JS Testing Framework](https://facebook.github.io/jest/docs/tutorial-react.html) 517 | * [B.E.M 101](https://css-tricks.com/bem-101/) 518 | * [React Responsive](https://github.com/contra/react-responsive) 519 | * [Storybook.JS](https://storybook.js.org/) 520 | 521 | ### Best Practice 522 | 523 | * [Ducks: Redux Reducer Bundles](https://github.com/erikras/ducks-modular-redux) 524 | * [Lessons from migrating a large codebase to React 16](https://blog.discordapp.com/lessons-from-migrating-a-large-codebase-to-react-16-e60e49102aa6) 525 | * [B.E.M: 10 Common Problems And How To Avoid Them](https://www.smashingmagazine.com/2016/06/battling-bem-extended-edition-common-problems-and-how-to-avoid-them/) 526 | * [Isomorphic JavaScript, The Future of Web Apps](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/) 527 | * [The Cost Of JavaScript](https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e) 528 | 529 | ## How to Contribute 530 | 531 | We welcome anyone to send us __pull request__ to improve this boilerplate, the goal is to make it better from time to time and we all can learn from it. 532 | 533 | This boilerplate will be maintained separately. So please do not check in any business logic to it unless it is for example purpose. 534 | 535 | ## Updates 536 | 537 | __04 / 30 / 2019__ 538 | 539 | Adding i18n / i10n support by adding [react-intl](https://github.com/yahoo/react-intl) 540 | 541 | __10 / 25 / 2018__ 542 | 543 | We are now on Webpack 4 and React 16.4, plus up-to-date with all our dependencies. 544 | 545 | We removed css-next in favor of postcss-preset-env. 546 | 547 | We removed extract-text-webpack-plugin to mini-css-extract-plugin because of Webpack 4 upgrade. 548 | 549 | __9 / 26 / 2017__ 550 | 551 | We are now on React 16, and up-to-date with all our dependencies. 552 | 553 | __9 / 14 / 2017__ 554 | 555 | For those of you seeing the following error 556 | 557 | `mozjpeg pre-build test failed ...` 558 | 559 | There is a bug in `imagemin` with Node 6 or above, and [here is the discussion](https://github.com/imagemin/imagemin/issues/168) 560 | 561 | If you are using mac, you can fix that by doing this 562 | 563 | `brew install automake autoconf libtool dpkg pkgconfig nasm libpng` 564 | 565 | ## License ?! 566 | In theory, knowledge should be free, so please visit [wtfpl](http://www.wtfpl.net/) for this boilerplate license if you really care. 567 | 568 | [david_img]: https://img.shields.io/david/iroy2000/react-redux-boilerplate.svg 569 | [david_site]: https://david-dm.org/iroy2000/react-redux-boilerplate 570 | -------------------------------------------------------------------------------- /__fixtures__/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux' 2 | 3 | export const getStore = (obj = {}) => createStore((state) => state, obj) 4 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /__tests__/common/components/Example.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Enzyme, { shallow } from 'enzyme' 3 | 4 | import { Example } from '../../../src/js/common/components/Example' 5 | 6 | const fixture = { 7 | example: { 8 | result: { 9 | testing: 'data', 10 | }, 11 | }, 12 | }; 13 | 14 | describe('ExampleView', () => { 15 | it('should render a blank div without data', () => { 16 | const el = shallow() 17 | 18 | expect(el.length).toEqual(1) 19 | expect(el.find('.exampleOutput').length).toEqual(0) 20 | }) 21 | 22 | it('should render with correct data', () => { 23 | const el = shallow( 24 | 25 | ) 26 | 27 | expect(el.length).toEqual(1) 28 | expect(el.find('.exampleOutput').length).toEqual(1) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /__tests__/redux/modules/example.js: -------------------------------------------------------------------------------- 1 | import { reducers, constants, actions, initialState } from '../../../src/js/redux/modules/example' 2 | import { getStore } from '../../../__fixtures__/store' 3 | 4 | const fixture = { 5 | title: 'fake-title', 6 | description: 'fake-description', 7 | source: 'fake-source', 8 | } 9 | 10 | describe('redux.modules.example', () => { 11 | let store = null; 12 | 13 | beforeEach(() => { 14 | store = getStore({ 15 | example: initialState() 16 | }); 17 | }); 18 | 19 | afterEach(() => { 20 | store = null; 21 | }) 22 | 23 | it('should return correct state when running updateExample', () => { 24 | const type = constants.UPDATE_EXAMPLE 25 | const state = store.getState().example 26 | 27 | const result = reducers[type](state, { payload: fixture }) 28 | 29 | expect(result.get('source')).toEqual(fixture.source) 30 | expect(result.get('title')).toEqual(fixture.title) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /__tests__/redux/sagas/exampleSagas.spec.js: -------------------------------------------------------------------------------- 1 | import { put } from 'redux-saga/effects' 2 | import { fetchExampleData } from '../../../src/js/redux/sagas/exampleSaga' 3 | import { actions as exampleActions } from '../../../src/js/redux/modules/example' 4 | 5 | describe('redux.sagas.exampleSaga', () => { 6 | describe('fetchExampleData', () => { 7 | 8 | global.__CONFIG__ = { 9 | description: 'fake description' 10 | } 11 | 12 | const fixture = { 13 | title: 'Everything is Awesome', 14 | description: __CONFIG__.description, 15 | source: 'This message is coming from Redux', 16 | }; 17 | 18 | it('should call exampleActions.updateExample with correct data', () => { 19 | const generator = fetchExampleData() 20 | 21 | let next = generator.next() 22 | 23 | expect(next.value).toEqual(put(exampleActions.updateExample(fixture))) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /__tests__/setup/jestTestSetup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /bin/commands.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | const config = require('config'); 3 | const colors = require('colors'); 4 | 5 | const option = process.argv[2]; 6 | 7 | // The following will allow you to add your own 8 | // - pre process 9 | // - post process 10 | // - parallel process 11 | // - logging 12 | // 13 | // You can add your process here and have package.json to proxy your command 14 | // Please look at package.json -> "scripts" section 15 | switch (option) { 16 | case 'lint': 17 | shell.exec('cross-env eslint --fix src/js/** --format node_modules/eslint-friendly-formatter . --ext .js --ext .jsx --cache; exit 0'); 18 | break; 19 | case 'dev': 20 | shell.exec(`cross-env webpack-dev-server --config webpack.config.dev-server.babel.js --hot --progress --no-info --inline --colors`); 21 | break; 22 | case 'build': 23 | shell.exec(`cross-env rimraf docroot && webpack --config webpack.config.build.babel.js --progress --display-error-details`); 24 | break; 25 | default: 26 | // If the app type is invalid, stop execution of the file. 27 | console.log(colors.green('Invalid option.')); 28 | console.log(colors.green('See README.md for more details.')); 29 | return; 30 | } 31 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "default", 3 | "host": "0.0.0.0", 4 | "port": "8080", 5 | "publicPath": "/", 6 | "assetPath": "assets", 7 | "jsSourcePath": "src/js", 8 | "optimization": { 9 | "analyzeMode": false, 10 | "analyze": { 11 | "port": 8888 12 | } 13 | }, 14 | "browserSync": { 15 | "active": true, 16 | "port": 3002 17 | }, 18 | "html": [ 19 | { 20 | "name": "homepage", 21 | "title": "Everything is Awesome", 22 | "template": "_default.html", 23 | "filename": "index.html" 24 | } 25 | ], 26 | "app": { 27 | "description": "config inside 'app' will be injected to your frontend code automatically" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "development", 3 | "app": { 4 | "description": "you can overwrite the default config", 5 | "hello": "You are in local development mode" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "production", 3 | "publicPath": "/" 4 | } 5 | -------------------------------------------------------------------------------- /config/stage.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "release", 3 | "publicPath": "./" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-boilerplate", 3 | "version": "1.3.4", 4 | "description": "React Redux Boilerplate is a workflow boilerplate that make life easier for developers by providing a virtual development environment and production ready build process framework out of the box.", 5 | "scripts": { 6 | "dev": "cross-env NODE_ENV=development DASHBOARD_PORT=9901 webpack-dashboard -p 9901 -c red -t dashboard -- node bin/commands.js dev", 7 | "build": "cross-env NODE_ENV=production node bin/commands.js build", 8 | "build:s3": "cross-env S3_DEPLOY=true NODE_ENV=production node bin/commands.js build", 9 | "build:stage": "cross-env NODE_ENV=stage node bin/commands.js build", 10 | "build:stage:s3": "cross-env S3_DEPLOY=true NODE_ENV=stage node bin/commands.js build", 11 | "clean": "rimraf docroot", 12 | "test": "jest --no-cache", 13 | "lint": "node bin/commands.js lint" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/iroy2000/react-redux-boilerplate.git" 18 | }, 19 | "homepage": "https://github.com/iroy2000/react-redux-boilerplate", 20 | "author": "Roy Yu ", 21 | "license": "MIT", 22 | "engines": { 23 | "npm": ">=3.10.7", 24 | "node": ">=6.9.0" 25 | }, 26 | "devDependencies": { 27 | "assets-webpack-plugin": "^3.9.7", 28 | "babel-cli": "^6.26.0", 29 | "babel-core": "^6.26.0", 30 | "babel-eslint": "^10.0.1", 31 | "babel-jest": "^23.4.0", 32 | "babel-loader": "^7.1.2", 33 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 34 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 35 | "babel-plugin-transform-runtime": "^6.23.0", 36 | "babel-plugin-typecheck": "^3.9.0", 37 | "babel-polyfill": "^6.26.0", 38 | "babel-preset-env": "^1.6.1", 39 | "babel-preset-flow": "^6.23.0", 40 | "babel-preset-react": "^6.24.1", 41 | "babel-preset-react-hmre": "^1.1.1", 42 | "babel-preset-react-optimize": "^1.0.1", 43 | "babel-preset-stage-0": "^6.24.1", 44 | "babel-register": "^6.26.0", 45 | "babel-runtime": "^6.26.0", 46 | "browser-sync": "^2.26.3", 47 | "browser-sync-webpack-plugin": "^2.2.2", 48 | "case-sensitive-paths-webpack-plugin": "^2.2.0", 49 | "colors": "^1.3.3", 50 | "concurrently": "^4.1.0", 51 | "config": "^3.0.1", 52 | "cross-env": "^5.2.0", 53 | "css-loader": "^2.1.0", 54 | "debug": "^4.1.1", 55 | "dotenv": "^6.2.0", 56 | "enzyme": "^3.9.0", 57 | "enzyme-adapter-react-16": "^1.9.1", 58 | "eslint": "^5.12.0", 59 | "eslint-config-airbnb": "^17.1.0", 60 | "eslint-config-prettier": "^4.2.0", 61 | "eslint-friendly-formatter": "^4.0.1", 62 | "eslint-plugin-flowtype": "^3.4.2", 63 | "eslint-plugin-import": "^2.16.0", 64 | "eslint-plugin-jsx-a11y": "^6.2.1", 65 | "eslint-plugin-prettier": "^3.0.1", 66 | "eslint-plugin-react": "^7.12.4", 67 | "file-loader": "^3.0.1", 68 | "html-webpack-plugin": "^3.2.0", 69 | "husky": "^1.3.1", 70 | "identity-obj-proxy": "^3.0.0", 71 | "image-webpack-loader": "^4.6.0", 72 | "jest-cli": "^23.4.1", 73 | "mini-css-extract-plugin": "^0.5.0", 74 | "postcss-loader": "^3.0.0", 75 | "postcss-preset-env": "^6.5.0", 76 | "precss": "^4.0.0", 77 | "prettier": "^1.17.0", 78 | "pretty-quick": "^1.10.0", 79 | "raf": "^3.4.1", 80 | "react-hot-loader": "^4.7.1", 81 | "react-test-renderer": "^16.2.0", 82 | "rimraf": "^2.6.3", 83 | "shelljs": "^0.8.3", 84 | "style-loader": "^0.23.1", 85 | "url-loader": "^1.1.2", 86 | "webpack": "^4.29.5", 87 | "webpack-bundle-analyzer": "^3.0.4", 88 | "webpack-cli": "^3.2.3", 89 | "webpack-dashboard": "^3.0.0", 90 | "webpack-dev-server": "^3.2.1", 91 | "webpack-s3-plugin": "^1.0.3" 92 | }, 93 | "dependencies": { 94 | "history": "^4.7.2", 95 | "immutable": "^4.0.0-rc.12", 96 | "prop-types": "^15.7.2", 97 | "react": "^16.8.3", 98 | "react-dom": "^16.8.3", 99 | "react-helmet": "^5.2.0", 100 | "react-intl": "^2.8.0", 101 | "react-loadable": "^5.5.0", 102 | "react-redux": "^7.0.3", 103 | "react-router": "^5.0.0", 104 | "react-router-dom": "^5.0.0", 105 | "react-router-redux": "^4.0.8", 106 | "redux": "^4.0.1", 107 | "redux-actions": "^2.6.4", 108 | "redux-saga": "^1.0.1", 109 | "reselect": "^4.0.0" 110 | }, 111 | "jest": { 112 | "testURL": "http://www.SomeAwesome.com", 113 | "verbose": true, 114 | "testPathIgnorePatterns": [ 115 | "/__tests__/setup/", 116 | "/node_modules/" 117 | ], 118 | "setupTestFrameworkScriptFile": "/__tests__/setup/jestTestSetup.js", 119 | "moduleNameMapper": { 120 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 121 | "\\.(css)$": "identity-obj-proxy" 122 | }, 123 | "setupFiles": [ 124 | "raf/polyfill" 125 | ], 126 | "automock": false, 127 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(js|jsx|json)$", 128 | "moduleFileExtensions": [ 129 | "js", 130 | "json", 131 | "jsx" 132 | ] 133 | }, 134 | "husky": { 135 | "hooks": { 136 | "pre-push": "npm test" 137 | } 138 | }, 139 | "keywords": [ 140 | "react", 141 | "redux", 142 | "webpack", 143 | "webpack-dev-server", 144 | "postcss", 145 | "jest", 146 | "boilerplate", 147 | "hmr" 148 | ] 149 | } 150 | -------------------------------------------------------------------------------- /src/assets/fonts/YourFavoriteFont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iroy2000/react-redux-boilerplate/94abd5ac8c51181a9c5e359d359a11315009a278/src/assets/fonts/YourFavoriteFont.eot -------------------------------------------------------------------------------- /src/assets/fonts/YourFavoriteFont.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iroy2000/react-redux-boilerplate/94abd5ac8c51181a9c5e359d359a11315009a278/src/assets/fonts/YourFavoriteFont.otf -------------------------------------------------------------------------------- /src/assets/template/_default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <%= htmlWebpackPlugin.options.title %> 13 | 14 | 15 | 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/js/README.md: -------------------------------------------------------------------------------- 1 | ## Before Your Start Coding 2 | 3 | This Boilerplate is providing a infrastructure to start your project as quickly as you can. 4 | 5 | __BUT__ before you start developing a real large scale project, please do ask yourself or your team a few questions :- 6 | 7 | * What is your domain layer looks like ? You can pretend redux as a data repository of everything, but it will provide no actual meaning for your application besides a data bag. For example, am I able to describe what a `User` is in the application. Having a domain layer will help yourself from having heart attack of arbitrary data structure that describe the same thing in different places. 8 | * I know you are a smart engineer, but please remember it is better to write code everyone can understand. 9 | * You only need to solve a problem when you have a real problem, don't prematurely making a decision based on preference. 10 | * Strong convention is better for everyone, if you don't have convention today, you will have N+1 conventions the next month. 11 | 12 | If you read to this point, I assumed you are a very serious developer, please help me to read the following links if you haven't! 13 | 14 | [SOLID JavaScript](http://aspiringcraftsman.com/2011/12/08/solid-javascript-single-responsibility-principle/) 15 | 16 | [Twelve Factor App](https://12factor.net/) 17 | -------------------------------------------------------------------------------- /src/js/Root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Provider } from 'react-redux' 4 | // You could use BrowserRoute or HashRoute 5 | // But passing in history directly to Route will 6 | // give your app more flexibility on deeper integration of `history` 7 | import { Router } from 'react-router-dom' 8 | 9 | import I18NProvider from 'common/components/Utilities/I18NProvider' 10 | 11 | export default class Root extends React.PureComponent { 12 | get content() { 13 | const { routes, history } = this.props 14 | 15 | return {routes} 16 | } 17 | 18 | render() { 19 | const { store } = this.props 20 | 21 | return ( 22 | 23 | {this.content} 24 | 25 | ) 26 | } 27 | } 28 | 29 | Root.propTypes = { 30 | history: PropTypes.object.isRequired, 31 | routes: PropTypes.element.isRequired, 32 | store: PropTypes.object.isRequired, 33 | } 34 | -------------------------------------------------------------------------------- /src/js/app-history.js: -------------------------------------------------------------------------------- 1 | // This is recommended way ( html5 browser history api ) 2 | // But for demo purpose, we diactivate that and use hashHistory instead 3 | // In order for your side to use this html history api, you need to do the following 4 | // 1) uncomment this line 5 | // 2) comment the line that says `createHashHistory'` 6 | // 3) go to webpack.config.dev-server and change `historyApiFallback` to true 7 | // 8 | // Please note that by using html5 history api, you need to setup your 9 | // static server to handle url accordingly. If you are not sure what it is, please 10 | // don't make changes in here. 11 | // import createHistory from 'history/createBrowserHistory' 12 | 13 | import { createHashHistory } from 'history'; 14 | 15 | const history = createHashHistory(); 16 | 17 | // Exposing history for deep integration needs 18 | // For example, saga and utilities 19 | export { history }; 20 | -------------------------------------------------------------------------------- /src/js/common/api/README.md: -------------------------------------------------------------------------------- 1 | ## Convention 2 | 3 | The following will be convention for this api/ 4 | 5 | * You can only put your api request here, no business logic. 6 | * You should separate your api files by "requested resources" 7 | * Your api should return a "Promise" interface for consistency 8 | * And you use `index.js` to expose your api 9 | 10 | This folder will contain an example using axios, it is not a working api, but show case how you should structure your apis. 11 | -------------------------------------------------------------------------------- /src/js/common/api/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | // This is an example will not work 3 | // This is just showing case how I would orangize 4 | export { 5 | getTopTenAvengers, 6 | } from './module/example' 7 | */ 8 | -------------------------------------------------------------------------------- /src/js/common/api/module/example.js: -------------------------------------------------------------------------------- 1 | /* 2 | // This is an example will not work 3 | // Just only show case how I would orangize 4 | const API_ENDPOINT = __CONFIG__.apiUrl.avengers 5 | 6 | export const getTopTenAvengers = (query = {}) => { 7 | return axios.get(`${API_ENDPOINT}?count=10`) 8 | } 9 | */ 10 | -------------------------------------------------------------------------------- /src/js/common/components/Example/Example.css: -------------------------------------------------------------------------------- 1 | @import "../../../../style/configs/color"; 2 | 3 | /* Example handler 4 | ---------------------- */ 5 | .exampleOutput { 6 | padding: 20px 60px 60px; 7 | border-radius: 6px; 8 | margin-bottom: 30px; 9 | color: inherit; 10 | background-color: #eee; 11 | 12 | h1 { 13 | font-size: 50px; 14 | .emphasize { 15 | font-size: 1.5em; 16 | font-weight: bold; 17 | @if 3 < 5 { 18 | color: $color-title; 19 | } 20 | @else { 21 | color: blue; 22 | } 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/js/common/components/Example/Example.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | import styles from './Example.css'; 4 | 5 | /** 6 | * Example 7 | * ============= 8 | * Besides it is acting as an example show case 9 | * Please also spend some time on the comments, it might 10 | * help you to grab more solid understanding of using React. 11 | * 12 | * Using React is easy, but using React correctly could be hard 13 | * 14 | * @extends PureComponent 15 | */ 16 | 17 | class Example extends PureComponent { 18 | /* eslint-disable max-len */ 19 | // 20 | // getDerivedStateFromProps is a replacement of componentWillReceiveProps 21 | // getDerivedStateFromProps exists for only one purpose. 22 | // It enables a component to update its internal state as the result of changes in props. 23 | // This method will run whenever parent is re-rendered 24 | // 25 | // static getDerivedStateFromProps(nextProps, prevState) { 26 | // } 27 | 28 | // getSnapshotBeforeUpdate is a replacement of componentWillUpdate 29 | // This is called right before the changes from VDOM is to be reflected in the DOM 30 | // If you return a value here, it will be the 3rd paramter of componentDidUpdate 31 | // 32 | // The new getSnapshotBeforeUpdate lifecycle is called right before mutations are made (e.g. before the DOM is updated). 33 | // The return value for this lifecycle will be passed as the third parameter to componentDidUpdate. 34 | // Note: This lifecycle isn’t often needed, but can be useful in cases like manually preserving scroll position during rerenders. 35 | // 36 | // getSnapshotBeforeUpdate(prevProps, prevState) { 37 | // 38 | // } 39 | // 40 | /* eslint-enable max-len */ 41 | 42 | render() { 43 | const { example } = this.props; 44 | const result = (example && example.result) || null; 45 | 46 | if (result) { 47 | return ( 48 |
49 |

50 | Let's Get 51 | Started 52 |

53 |

If you see this screen, it means you are all setup \o/

54 |

55 | The following JSON are showing contents coming from Redux, Saga and 56 | Config. 57 |

58 |
{JSON.stringify(result, undefined, 2)}
59 |
60 | ); 61 | } 62 | return
; 63 | } 64 | } 65 | 66 | export default Example; 67 | -------------------------------------------------------------------------------- /src/js/common/components/Example/ExampleWithError.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | import styles from './Example.css'; 4 | 5 | class ExampleWithError extends PureComponent { 6 | render() { 7 | const { example } = this.props; 8 | const result = example && example.result ? example.result : null; 9 | 10 | if (result && result.size && result.size > 0) { 11 | return ( 12 |
13 |

This should catch by ErrorBoundary

14 | {result.something_not_existed.get('something_not_existed')} 15 |
16 | ); 17 | } 18 | return
; 19 | } 20 | } 21 | 22 | export default ExampleWithError; 23 | -------------------------------------------------------------------------------- /src/js/common/components/Example/index.js: -------------------------------------------------------------------------------- 1 | export { default as Example } from './Example'; 2 | export { default as ExampleWithError } from './ExampleWithError'; 3 | -------------------------------------------------------------------------------- /src/js/common/components/Header/Header.css: -------------------------------------------------------------------------------- 1 | .globalHeader { 2 | ul { 3 | list-style: none; 4 | margin-left: -40px; 5 | margin-top: 10px; 6 | li { 7 | float: left; 8 | margin-right: 10px; 9 | font-size: 15px; 10 | border-bottom: 3px solid #fff; 11 | cursor: default; 12 | font-weight: bold; 13 | 14 | &.active:hover { 15 | border-bottom: 3px solid #721c24; 16 | } 17 | 18 | a { 19 | text-decoration: none; 20 | color: #721c24; 21 | } 22 | } 23 | 24 | &:after { 25 | clear: both; 26 | content: ""; 27 | display: table; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/js/common/components/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import styles from './Header.css'; 5 | 6 | class Header extends PureComponent { 7 | render() { 8 | const { location } = this.props; 9 | const { pathname } = location; 10 | 11 | const isHome = pathname === '/'; 12 | const isJustAnotherPage = pathname === '/page'; 13 | 14 | return ( 15 |
16 |
    17 |
  • 18 | {isHome ? 'Home' : Home} 19 |
  • 20 |
  • 21 | {isJustAnotherPage ? ( 22 | 'Just Another Page' 23 | ) : ( 24 | Just Another Page 25 | )} 26 |
  • 27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | export default Header; 34 | -------------------------------------------------------------------------------- /src/js/common/components/Header/index.js: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header'; 2 | -------------------------------------------------------------------------------- /src/js/common/components/LazyLoading/Loading.css: -------------------------------------------------------------------------------- 1 | .loader { 2 | color: #f07d00; 3 | font-size: 20px; 4 | margin: 100px auto; 5 | width: 1em; 6 | height: 1em; 7 | border-radius: 50%; 8 | position: relative; 9 | text-indent: -9999em; 10 | -webkit-animation: spinner 1s infinite linear; 11 | animation: spinner 1s infinite linear; 12 | -webkit-transform: translateZ(0); 13 | -ms-transform: translateZ(0); 14 | transform: translateZ(0); 15 | } 16 | @keyframes spinner { 17 | 0%, 18 | 100% { 19 | box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0; 20 | } 21 | 12.5% { 22 | box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em; 23 | } 24 | 25% { 25 | box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em; 26 | } 27 | 37.5% { 28 | box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; 29 | } 30 | 50% { 31 | box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; 32 | } 33 | 62.5% { 34 | box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; 35 | } 36 | 75% { 37 | box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; 38 | } 39 | 87.5% { 40 | box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/js/common/components/LazyLoading/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | import styles from './Loading.css'; 4 | 5 | class Loading extends PureComponent { 6 | getMessage() { 7 | const { 8 | isLoading, timedOut, pastDelay, error, 9 | } = this.props; 10 | 11 | const errorMessage = 'We can't pull up information at this point, please try again.'; 12 | 13 | if (isLoading) { 14 | if (timedOut) { 15 | return
{errorMessage}
; 16 | } 17 | if (pastDelay) { 18 | return
Loading...
; 19 | } 20 | return null; 21 | } 22 | if (error) { 23 | return
{errorMessage}
; 24 | } 25 | 26 | return null; 27 | } 28 | 29 | render() { 30 | return this.getMessage(); 31 | } 32 | } 33 | 34 | export default Loading; 35 | -------------------------------------------------------------------------------- /src/js/common/components/LazyLoading/index.js: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable' 2 | 3 | import Loading from './Loading' 4 | 5 | export const create = (loader) => Loadable({ 6 | loader, 7 | loading: Loading, 8 | }) 9 | 10 | export default create 11 | -------------------------------------------------------------------------------- /src/js/common/components/Utilities/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | 3 | const style = { 4 | padding: '.75rem 1.25rem', 5 | marginBottom: '1rem', 6 | border: '1px solid transparent', 7 | borderRadius: '.25rem', 8 | color: '#721c24', 9 | backgroundColor: '#f8d7da', 10 | borderColor: '#f5c6cb', 11 | } 12 | 13 | class ErrorBoundary extends PureComponent { 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | hasError: false, 18 | } 19 | } 20 | 21 | componentDidCatch(error, info) { 22 | // you probably want to log it somewhere 23 | console.log(error, info) 24 | this.setState({ 25 | hasError: true, 26 | }) 27 | } 28 | 29 | render() { 30 | const { hasError } = this.state 31 | const { children } = this.props 32 | 33 | if (hasError) { 34 | return ( 35 |
36 | Something is going wrong!! This error is caught by ErrorBoundary 37 | feature in React 16 \o/ 38 |
39 | ) 40 | } 41 | 42 | return children 43 | } 44 | } 45 | 46 | export default ErrorBoundary 47 | -------------------------------------------------------------------------------- /src/js/common/components/Utilities/I18NProvider.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { IntlProvider, addLocaleData } from 'react-intl' 4 | 5 | // This is react-intl locale data 6 | import en from 'react-intl/locale-data/en' 7 | 8 | // This is your translation files 9 | // In case you are curious about locale - https://gist.github.com/jacobbubu/1836273 10 | import enUS from 'common/translations/en-US.json' 11 | 12 | // We are adding english here 13 | addLocaleData([...en]); 14 | 15 | // Creating a map of supported messages 16 | // It will be used in IntlProvider below 17 | const messages = { 18 | 'en-US': enUS, 19 | } 20 | 21 | export default class I18NProvider extends React.PureComponent { 22 | static propTypes = { 23 | children: PropTypes.element.isRequired, 24 | } 25 | 26 | render() { 27 | // query the browser for language / locale 28 | // feel free to modify this logic to fit your need 29 | const language = navigator.language.split(/[-_]/)[0]; 30 | const locale = navigator.language; 31 | 32 | const { children } = this.props 33 | 34 | return ( 35 | 36 | { children } 37 | 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/js/common/components/Utilities/index.js: -------------------------------------------------------------------------------- 1 | export { default as ErrorBoundary } from './ErrorBoundary'; 2 | -------------------------------------------------------------------------------- /src/js/common/translations/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "greetings.hello": "Hello, {name}, this is a message from translations!" 3 | } 4 | -------------------------------------------------------------------------------- /src/js/common/types/example.js: -------------------------------------------------------------------------------- 1 | export type exampleType = { 2 | title: string, 3 | description: string, 4 | source: string 5 | }; 6 | -------------------------------------------------------------------------------- /src/js/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { fromJS } from 'immutable' 4 | 5 | import routes from './routes' 6 | import Root from './Root' 7 | import configureStore from './redux/configureStore' 8 | import { history } from './app-history' 9 | 10 | let initialState = {} 11 | 12 | // rehydrate initialState for JS app 13 | if (window.__INITIAL_STATE__) { 14 | initialState = window.__INITIAL_STATE__ 15 | 16 | // Transform into Immutable.js collections, 17 | // but leave top level keys untouched for Redux 18 | Object.keys(initialState).forEach((key) => { 19 | initialState[key] = fromJS(initialState[key]) 20 | }) 21 | } 22 | 23 | const store = configureStore(initialState, history) 24 | 25 | // Render the React application to the DOM 26 | // Root component is to bootstrap Provider, Router and DevTools 27 | ReactDOM.render( 28 | , 29 | document.getElementById('app-container') 30 | ) 31 | -------------------------------------------------------------------------------- /src/js/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | import createSagaMiddleware from 'redux-saga' 2 | import { applyMiddleware, compose, createStore } from 'redux' 3 | 4 | import sagas from './sagas' 5 | import rootReducer from './rootReducers' 6 | 7 | // Redux DevTools Extension for Chrome and Firefox 8 | const reduxDevTool = () => { 9 | return typeof window === 'object' 10 | && typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined' 11 | ? window.__REDUX_DEVTOOLS_EXTENSION__() 12 | : (f) => f 13 | } 14 | 15 | // history is passed here, for this example, we don't use history 16 | export default function configureStore(initialState, history) { // eslint-disable-line no-unused-vars, max-len 17 | const sagaMiddleware = createSagaMiddleware() 18 | 19 | const middleware = applyMiddleware(sagaMiddleware) 20 | 21 | const composedStoreEnhancer = compose( 22 | middleware, 23 | reduxDevTool() 24 | ) 25 | 26 | const store = composedStoreEnhancer(createStore)(rootReducer, initialState) 27 | 28 | sagaMiddleware.run(sagas) 29 | 30 | if (module.hot) { 31 | module.hot.accept('./rootReducers', () => { 32 | store.replaceReducer(require('./rootReducers')) 33 | }) 34 | } 35 | 36 | return store 37 | } 38 | -------------------------------------------------------------------------------- /src/js/redux/modules/example.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions' 2 | import { Map } from 'immutable' 3 | 4 | import type { exampleType } from '../../common/types/example' 5 | 6 | const GET_EXAMPLE = 'app/example/GET_EXAMPLE' 7 | const UPDATE_EXAMPLE = 'app/example/UPDATE_EXAMPLE' 8 | 9 | export const constants = { 10 | GET_EXAMPLE, 11 | UPDATE_EXAMPLE, 12 | } 13 | 14 | // ------------------------------------ 15 | // Actions 16 | // ------------------------------------ 17 | export const getAwesomeCode = createAction(GET_EXAMPLE, () => ({})) 18 | export const updateExample = createAction( 19 | UPDATE_EXAMPLE, 20 | (result: exampleType) => ({ result }) 21 | ) 22 | 23 | export const actions = { 24 | getAwesomeCode, 25 | updateExample, 26 | } 27 | 28 | export const reducers = { 29 | [UPDATE_EXAMPLE]: (state, { payload }) => state.merge({ 30 | ...payload, 31 | }), 32 | } 33 | 34 | export const initialState = () => Map({ 35 | result: '', 36 | }) 37 | 38 | export default handleActions(reducers, initialState()) 39 | -------------------------------------------------------------------------------- /src/js/redux/modules/index.js: -------------------------------------------------------------------------------- 1 | export { default as example } from './example'; 2 | -------------------------------------------------------------------------------- /src/js/redux/rootReducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import example from './modules/example' 3 | 4 | export default combineReducers({ 5 | example, 6 | }) 7 | -------------------------------------------------------------------------------- /src/js/redux/sagas/exampleSaga.js: -------------------------------------------------------------------------------- 1 | import { put, fork, takeLatest } from 'redux-saga/effects' 2 | import { 3 | constants as exampleConstants, 4 | actions as exampleActions, 5 | } from '../modules/example' 6 | 7 | import type { exampleType } from '../../common/types/example' 8 | 9 | export function* fetchExampleData() { 10 | // pretend there is an api call 11 | const result: exampleType = { 12 | title: 'Everything is Awesome', 13 | description: __CONFIG__.description, 14 | source: 'This message is coming from Redux', 15 | } 16 | 17 | yield put(exampleActions.updateExample(result)) 18 | } 19 | 20 | function* watchGetExample() { 21 | yield takeLatest(exampleConstants.GET_EXAMPLE, fetchExampleData) 22 | } 23 | 24 | export const exampleSaga = [fork(watchGetExample)] 25 | -------------------------------------------------------------------------------- /src/js/redux/sagas/index.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | import { exampleSaga } from './exampleSaga'; 3 | 4 | export default function* sagas() { 5 | yield all([...exampleSaga]); 6 | } 7 | -------------------------------------------------------------------------------- /src/js/redux/selectors/exampleSelector.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | const exampleDataSelector = (state) => state.example 4 | 5 | const resultSelector = createSelector( 6 | exampleDataSelector, 7 | (payload) => payload.get('result') 8 | ) 9 | 10 | export const exampleSelector = (state) => ({ 11 | result: resultSelector(state), 12 | }) 13 | -------------------------------------------------------------------------------- /src/js/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Switch, withRouter } from 'react-router-dom' 3 | 4 | import LazyLoading from 'common/components/LazyLoading' 5 | 6 | import styles from '../style/index.css' 7 | 8 | // This is show case how you can lazy loading component 9 | const ExampleRouteHandler = LazyLoading(() => import('views/example')) 10 | const Header = LazyLoading(() => import('common/components/Header/Header')) 11 | 12 | // Please remove that, it is an example 13 | const JustAnotherPage = () => ( 14 |
15 |

This is Just Another Page

16 |

17 | Please remove this from your route, it is just to show case basic setup 18 | for router. 19 |

20 |
21 | ) 22 | 23 | // This show case how you can access routing info in your component 24 | const HeaderWithRouter = withRouter((props) =>
) 25 | 26 | module.exports = ( 27 |
28 | 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 | ) 39 | -------------------------------------------------------------------------------- /src/js/views/README.md: -------------------------------------------------------------------------------- 1 | ## Convention 2 | 3 | The organization of `views` is suggested as follows: 4 | 5 | * Folders correlate to the most significant portion of the URL to map to in case and spelling within the application, and not including the application mount point if there is one. 6 | * Example: if a URL named `/avengers-search/` maps to a page, then we should have a `views/avengers-search/` folder that exports a component that manages the `avengers-search` page. 7 | * NOTE: Due to React component naming requirements, the variable that references the "page" should always be PascalCased, e.g. avengers-search -> AvengersSearch. 8 | * Each folder should make use of the `index.js` trick to allow importing of the folder. 9 | * Major sub views that correlate to URLs should, if appropriate, also be organized within the parent view folder. 10 | * Besides `index.js`, the main coordinating file should be named `View.jsx`, and this is the file that should be exported as default from `index.js`. 11 | -------------------------------------------------------------------------------- /src/js/views/example/View.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | 5 | // This is i18n and i10n 6 | import { FormattedMessage, FormattedDate, FormattedTime } from 'react-intl' 7 | 8 | import LazyLoading from '../../common/components/LazyLoading' 9 | import { actions as exampleActions } from '../../redux/modules/example' 10 | import { exampleSelector } from '../../redux/selectors/exampleSelector' 11 | import { ExampleWithError } from '../../common/components/Example'; 12 | import { ErrorBoundary } from '../../common/components/Utilities'; 13 | 14 | // This is lazy loading example 15 | const LazyExample = LazyLoading(() => import('../../common/components/Example/Example')); 16 | 17 | class ExampleView extends Component { 18 | static propTypes = { 19 | example: PropTypes.object.isRequired, 20 | } 21 | 22 | state = { 23 | myArbitraryNumber: Math.floor(Math.random() * 10000), 24 | currentTime: new Date(), 25 | } 26 | 27 | componentDidMount() { 28 | const { getAwesomeCode } = this.props 29 | 30 | getAwesomeCode() 31 | } 32 | 33 | render() { 34 | const { myArbitraryNumber, currentTime } = this.state 35 | 36 | // Note for i18n and i10n 37 | // if `id` is found, it will use the matched message 38 | // otherwise, it will use defaultMessage as fallback 39 | 40 | return ( 41 | 42 | 43 |

This framework supports i18n and i10n out of the box.

44 |

45 | Visitor, 50 | }} 51 | /> 52 |

53 |

54 | 64 |

65 |

66 | The date is:   67 | 68 |

69 |

70 | The time is:   71 | 72 |

73 | 74 | 75 | 76 |
77 | ) 78 | } 79 | } 80 | 81 | const mapStateToProps = (state) => ({ 82 | example: exampleSelector(state), 83 | }) 84 | 85 | const mapDispatchToProps = { 86 | ...exampleActions, 87 | } 88 | 89 | export default connect( 90 | mapStateToProps, 91 | mapDispatchToProps 92 | )(ExampleView) 93 | -------------------------------------------------------------------------------- /src/js/views/example/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './View'; 2 | -------------------------------------------------------------------------------- /src/style/configs/color.css: -------------------------------------------------------------------------------- 1 | $color-title: #721c24; 2 | -------------------------------------------------------------------------------- /src/style/configs/font-faces.css: -------------------------------------------------------------------------------- 1 | /* 2 | // This file is just an example, please delete assets/fonts/YourFavoriteFont when you install 3 | // You probably need a different fonts for your site :) 4 | // 5 | // And in order to setup ultimate and correct font-faces, please use the following link 6 | // https://css-tricks.com/snippets/css/using-font-face/ 7 | // 8 | // The font-face here is just to show the setup is working 9 | */ 10 | 11 | @font-face { 12 | font-family: "YourFavoriteFont"; 13 | src: url("../assets/fonts/YourFavoriteFont.eot") format("embedded-opentype"); 14 | src: url("../assets/fonts/YourFavoriteFont.otf") format("opentype"); 15 | font-weight: normal; 16 | font-style: normal; 17 | } 18 | -------------------------------------------------------------------------------- /src/style/index.css: -------------------------------------------------------------------------------- 1 | @import "configs/color"; 2 | @import "configs/font-faces"; 3 | 4 | .container { 5 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 6 | font-size: 16px; 7 | margin-left: 60px; 8 | margin-right: 60px; 9 | 10 | .content { 11 | 12 | } 13 | 14 | h1, h2 { 15 | /* This is how you use custom fonts */ 16 | font-family: YourFavoriteFont; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.build.babel.js: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import path from 'path' 3 | import webpack from 'webpack' 4 | import HtmlWebpackPlugin from 'html-webpack-plugin' 5 | import SaveAssetsJson from 'assets-webpack-plugin' 6 | import MiniCssExtractPlugin from 'mini-css-extract-plugin' 7 | import precss from 'precss' 8 | import postcssPresetEnv from 'postcss-preset-env' 9 | import AWS from 'aws-sdk' 10 | 11 | 12 | import webpackConfig, { JS_SOURCE } from './webpack.config.common' 13 | 14 | // ---------------------------------------------------------- 15 | // CONSTANT DECLARATION 16 | // ---------------------------------------------------------- 17 | const IS_S3_DEPLOY = Boolean(process.env.S3_DEPLOY); 18 | const PUBLIC_PATH = IS_S3_DEPLOY ? process.env.AWS_CDN_URL : config.get('publicPath'); 19 | const APP_ENTRY_POINT = `${JS_SOURCE}/main`; 20 | 21 | // webpack 4 mode 22 | // https://webpack.js.org/concepts/mode/ 23 | webpackConfig.mode = 'production' 24 | 25 | const webpackProdOutput = { 26 | publicPath: PUBLIC_PATH, 27 | filename: `${config.get('assetPath')}/[name]-[hash].js`, 28 | chunkFilename: `${config.get('assetPath')}/[id].[hash].js`, 29 | } 30 | 31 | const html = config.get('html') 32 | 33 | // Please configure this section if you plan 34 | // to deploy the generated html to production. 35 | // I don't mind you name your page as Retro 36 | // if you want to ... 37 | const htmlPlugins = html.map( 38 | (page) => new HtmlWebpackPlugin({ 39 | title: page.title, 40 | template: `src/assets/template/${page.template}`, 41 | inject: 'body', 42 | filename: page.filename, 43 | minify: { 44 | removeComments: true, 45 | collapseWhitespace: true, 46 | conservativeCollapse: true, 47 | }, 48 | }) 49 | ) 50 | 51 | // ---------------------------------------------------------- 52 | // Extending Webpack Configuration 53 | // ---------------------------------------------------------- 54 | 55 | // Merges webpackProdOutput and webpackConfig.output 56 | webpackConfig.output = Object.assign(webpackConfig.output, webpackProdOutput) 57 | 58 | webpackConfig.module.rules = webpackConfig.module.rules.concat({ 59 | test: /\.css$/, 60 | use: [ 61 | { 62 | loader: MiniCssExtractPlugin.loader, 63 | }, 64 | { 65 | loader: 'css-loader', 66 | options: { 67 | sourceMap: true, 68 | importLoaders: 1, 69 | modules: true, 70 | localIdentName: '[name]__[local]_[hash:base64]', 71 | }, 72 | }, 73 | { 74 | loader: 'postcss-loader', 75 | ident: 'postcss', 76 | options: { 77 | sourceMap: true, 78 | plugins: () => [ 79 | precss(), 80 | postcssPresetEnv({ 81 | browsers: ['last 2 versions', 'ie >= 9'], 82 | compress: true, 83 | }), 84 | ], 85 | }, 86 | }, 87 | ], 88 | }) 89 | 90 | webpackConfig.devtool = 'source-map' 91 | 92 | webpackConfig.entry = { 93 | app: ['babel-polyfill', path.resolve(__dirname, APP_ENTRY_POINT)], 94 | } 95 | 96 | if (IS_S3_DEPLOY) { 97 | const S3Plugin = require('webpack-s3-plugin') 98 | 99 | // Please read README if you have no idea where 100 | // `process.env.AWS_ACCESS_KEY` is coming from 101 | let s3Options = {}; 102 | if (process.env.AWS_PROFILE) { 103 | s3Options = new AWS.SharedIniFileCredentials({ profile: process.env.AWS_PROFILE }); 104 | } 105 | if (process.env.AWS_ACCESS_KEY) { 106 | s3Options.accessKeyId = process.env.AWS_ACCESS_KEY_ID; 107 | } 108 | if (process.env.AWS_SECRET_KEY) { 109 | s3Options.secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; 110 | } 111 | const s3Config = new S3Plugin({ 112 | // Only upload css and js 113 | // include: /.*\.(css|js)/, 114 | // s3Options are required 115 | ...s3Options, 116 | s3UploadOptions: { 117 | Bucket: process.env.AWS_BUCKET, 118 | }, 119 | cdnizerCss: { 120 | test: /images/, 121 | cdnUrl: process.env.AWS_CDN_URL, 122 | }, 123 | }) 124 | 125 | webpackConfig.plugins = webpackConfig.plugins.concat(s3Config) 126 | } 127 | 128 | if (config.get('optimization.analyzeMode') === true) { 129 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') 130 | 131 | webpackConfig.plugins = webpackConfig.plugins.concat( 132 | new BundleAnalyzerPlugin({ 133 | analyzerMode: 'server', 134 | analyzerHost: 'localhost', 135 | analyzerPort: config.get('optimization.analyze.port'), 136 | openAnalyzer: true, 137 | }) 138 | ) 139 | } 140 | 141 | webpackConfig.plugins.push( 142 | new webpack.DefinePlugin({ 143 | __CONFIG__: JSON.stringify(config.get('app')), 144 | }), 145 | new webpack.LoaderOptionsPlugin({ 146 | minimize: true, 147 | debug: false, 148 | }), 149 | // how you want your code to be optimized 150 | // all configurable 151 | new webpack.IgnorePlugin(/un~$/), 152 | 153 | new SaveAssetsJson({ 154 | path: path.join(__dirname, 'docroot'), 155 | filename: 'assets.json', 156 | prettyPrint: true, 157 | metadata: { 158 | version: process.env.PACKAGE_VERSION, 159 | }, 160 | }), 161 | new MiniCssExtractPlugin({ 162 | // Options similar to the same options in webpackOptions.output 163 | // both options are optional 164 | filename: `${config.get('assetPath')}/[name]-[hash].css`, 165 | chunkFilename: `${config.get('assetPath')}/[id]-[hash].css`, 166 | }) 167 | ) 168 | 169 | webpackConfig.plugins = webpackConfig.plugins.concat(htmlPlugins) 170 | 171 | export default webpackConfig 172 | -------------------------------------------------------------------------------- /webpack.config.common.js: -------------------------------------------------------------------------------- 1 | // Requiring dependencies 2 | // ================================================================================ 3 | import path from 'path'; 4 | import webpack from 'webpack'; 5 | import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; 6 | import config from 'config'; 7 | 8 | // Please read the following link if 9 | // you have no idea how to use this feature 10 | // https://github.com/motdotla/dotenv 11 | require('dotenv').config({ silent: true }); 12 | 13 | // trace which loader is deprecated 14 | // feel free to remove that if you don't need this feature 15 | process.traceDeprecation = false; 16 | 17 | // Environment variable injection 18 | // ================================================================================ 19 | import packageJSON from './package.json' 20 | process.env.PACKAGE_VERSION = packageJSON.version 21 | 22 | // Defining config variables 23 | // ================================================================================ 24 | 25 | export const BUILD_PATH = path.join(__dirname, `docroot${config.get('publicPath')}`) 26 | 27 | const COMMON_LOADERS = [ 28 | { 29 | test: /\.(?:ico|gif|png|jpg|jpeg|webp|svg)$/i, 30 | use: [ 31 | { 32 | loader: 'file-loader', 33 | options: { 34 | hash: 'sha512', 35 | digest: 'hex', 36 | name: `${config.get('assetPath')}/[hash].[ext]`, 37 | } 38 | }, 39 | { 40 | loader: 'image-webpack-loader', 41 | options: { 42 | query: { 43 | mozjpeg: { 44 | progressive: true, 45 | }, 46 | gifsicle: { 47 | interlaced: true, 48 | }, 49 | optipng: { 50 | optimizationLevel: 7, 51 | }, 52 | pngquant: { 53 | quality: '65-90', 54 | speed: 4 55 | } 56 | }, 57 | } 58 | } 59 | ], 60 | }, { 61 | test: /\.(js|jsx)?$/, 62 | exclude: /node_modules/, 63 | loader: 'babel-loader', 64 | options: { 65 | cacheDirectory: true, 66 | plugins: [ 67 | 'transform-runtime', 68 | 'transform-decorators-legacy', 69 | 'syntax-dynamic-import' 70 | ], 71 | }, 72 | }, 73 | { 74 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 75 | use: [ 76 | { 77 | loader: 'url-loader', 78 | options: { 79 | limit: 10000, 80 | mimetype: 'application/font-woff', 81 | name: `${config.get('assetPath')}/[name].[ext]`, 82 | } 83 | } 84 | ], 85 | }, 86 | { 87 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 88 | use: [ 89 | { 90 | loader: 'url-loader', 91 | options: { 92 | limit: 10000, 93 | mimetype: 'application/font-woff', 94 | name: `${config.get('assetPath')}/[name].[ext]`, 95 | } 96 | } 97 | ], 98 | }, 99 | { 100 | test: /\.[ot]tf(\?v=\d+\.\d+\.\d+)?$/, 101 | use: [ 102 | { 103 | loader: 'url-loader', 104 | options: { 105 | limit: 10000, 106 | mimetype: 'application/octet-stream', 107 | name: `${config.get('assetPath')}/[name].[ext]`, 108 | } 109 | } 110 | ], 111 | }, 112 | { 113 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 114 | use: [ 115 | { 116 | loader: 'url-loader', 117 | options: { 118 | limit: 10000, 119 | mimetype: 'application/vnd.ms-fontobject', 120 | name: `${config.get('assetPath')}/[name].[ext]`, 121 | } 122 | } 123 | ], 124 | } 125 | ]; 126 | 127 | // Export 128 | // =============================================================================== 129 | export const JS_SOURCE = config.get('jsSourcePath'); 130 | 131 | export default { 132 | output: { 133 | path: path.join(__dirname, 'docroot'), 134 | }, 135 | performance: { 136 | hints: process.env.NODE_ENV === 'production' ? "warning" : false 137 | }, 138 | optimization: { 139 | splitChunks: { 140 | chunks: 'all' 141 | } 142 | }, 143 | resolve: { 144 | extensions: ['.js', '.jsx', '.css'], 145 | modules: [ 146 | path.join(__dirname, 'src'), 147 | path.join(__dirname, 'assets'), 148 | path.join(__dirname, JS_SOURCE), 149 | "node_modules" 150 | ], 151 | }, 152 | plugins: [ 153 | new webpack.IgnorePlugin(/vertx/), // https://github.com/webpack/webpack/issues/353 154 | new CaseSensitivePathsPlugin(), 155 | ], 156 | module: { 157 | rules: COMMON_LOADERS, 158 | }, 159 | node: { 160 | console: true, 161 | fs: 'empty', 162 | net: 'empty', 163 | tls: 'empty' 164 | }, 165 | externals: { 166 | console:true, 167 | fs:'{}', 168 | tls:'{}', 169 | net:'{}' 170 | }, 171 | }; 172 | -------------------------------------------------------------------------------- /webpack.config.dev-server.babel.js: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import webpack from 'webpack' 3 | import HtmlWebpackPlugin from 'html-webpack-plugin' 4 | import DashboardPlugin from 'webpack-dashboard/plugin' 5 | import precss from 'precss' 6 | import postcssPresetEnv from 'postcss-preset-env' 7 | 8 | import webpackConfig, { JS_SOURCE } from './webpack.config.common' 9 | 10 | // Please read the following link if 11 | // you have no idea how to use this feature 12 | // https://github.com/motdotla/dotenv 13 | require('dotenv').config({ silent: true }) 14 | 15 | const HOST = process.env.HOST || config.get('host') || '0.0.0.0' 16 | const PORT = process.env.PORT || config.get('port') || '8080' 17 | const APP_ENTRY_POINT = `${JS_SOURCE}/main` 18 | 19 | const webpackDevOutput = { 20 | publicPath: config.get('publicPath'), 21 | filename: 'bundle.js', 22 | } 23 | 24 | // webpack 4 mode 25 | // https://webpack.js.org/concepts/mode/ 26 | webpackConfig.mode = 'development' 27 | 28 | // Merges webpackDevOutput and webpackConfig.output 29 | webpackConfig.output = Object.assign(webpackConfig.output, webpackDevOutput) 30 | 31 | webpackConfig.devServer = { 32 | host: HOST, 33 | port: PORT, 34 | // please look at app-history 35 | // this config is using HTML5 History API 36 | // If you would like to switch back to browser history, 37 | // you can turn this to true, and modify app-history accordingly. 38 | historyApiFallback: false, 39 | disableHostCheck: true, 40 | clientLogLevel: 'error', 41 | compress: true, 42 | noInfo: true, 43 | quiet: true, 44 | open: true, 45 | stats: 'errors-only', 46 | } 47 | 48 | // This is your testing container, we did 49 | // that for you, so you don't need to, if 50 | // you need to change the container template 51 | // go to the file in `template` below 52 | const html = config.get('html') 53 | 54 | const htmlPlugins = html.map( 55 | (page) => new HtmlWebpackPlugin({ 56 | title: page.title, 57 | template: `src/assets/template/${page.template}`, 58 | inject: 'body', 59 | filename: page.filename, 60 | }) 61 | ) 62 | 63 | webpackConfig.plugins.push( 64 | new DashboardPlugin({ 65 | port: process.env.DASHBOARD_PORT, 66 | minified: false, 67 | gzip: false, 68 | }), 69 | new webpack.LoaderOptionsPlugin({ 70 | debug: true, 71 | }), 72 | // Since we specify --hot mode, we don’t need to add this plugin 73 | // It is mutually exclusive with the --hot option. 74 | // new webpack.HotModuleReplacementPlugin(), 75 | new webpack.DefinePlugin({ 76 | __CONFIG__: JSON.stringify(config.get('app')), 77 | }) 78 | ) 79 | 80 | // We turn off browserSync by default 81 | // Turn that on if you want to include this use case 82 | if (config.get('browserSync.active') === true) { 83 | const BrowserSyncPlugin = require('browser-sync-webpack-plugin') 84 | webpackConfig.plugins.push( 85 | new BrowserSyncPlugin( 86 | { 87 | host: 'localhost', 88 | port: config.get('browserSync.port'), 89 | proxy: `http://localhost:${process.env.PORT}/`, 90 | 91 | // Prevents BrowserSync from automatically opening up the app in your browser 92 | open: false, 93 | reloadDelay: 2500, 94 | }, 95 | { 96 | // Disable BrowserSync's browser reload/asset injections feature because 97 | // Webpack Dev Server handles this for us already 98 | reload: false, 99 | } 100 | ) 101 | ) 102 | } 103 | 104 | webpackConfig.module.rules = webpackConfig.module.rules.concat({ 105 | test: /\.css$/, 106 | use: [ 107 | { 108 | loader: 'style-loader', 109 | }, 110 | { 111 | loader: 'css-loader', 112 | options: { 113 | sourceMap: true, 114 | importLoaders: 1, 115 | modules: true, 116 | localIdentName: '[name]__[local]_[hash:base64]', 117 | }, 118 | }, 119 | { 120 | loader: 'postcss-loader', 121 | ident: 'postcss', 122 | options: { 123 | sourceMap: true, 124 | // https://github.com/postcss/postcss-loader/issues/92 125 | // https://github.com/postcss/postcss-loader/issues/8 126 | plugins: () => [ 127 | precss(), 128 | // https://github.com/csstools/postcss-preset-env 129 | postcssPresetEnv({ 130 | browsers: ['last 2 versions', 'ie >= 9'], 131 | compress: true, 132 | }), 133 | ], 134 | }, 135 | }, 136 | ], 137 | }) 138 | 139 | webpackConfig.plugins = webpackConfig.plugins.concat(htmlPlugins) 140 | 141 | // webpack 4, if you set mode = 'development', it will set this value 142 | // webpackConfig.devtool = 'cheap-module-eval-source-map'; 143 | 144 | webpackConfig.entry = [ 145 | 'babel-polyfill', 146 | `webpack-dev-server/client?http://${HOST}:${PORT}`, 147 | 'webpack/hot/only-dev-server', 148 | `./${APP_ENTRY_POINT}`, 149 | ] 150 | 151 | export default webpackConfig 152 | --------------------------------------------------------------------------------