Browse Source

Initial commit.

pull/1/head
Titouan Rigoudy 9 years ago
commit
ea9336ec2c
38 changed files with 1496 additions and 0 deletions
  1. +19
    -0
      .babelrc
  2. +13
    -0
      .editorconfig
  3. +65
    -0
      .eslintrc
  4. +36
    -0
      .gitignore
  5. +1
    -0
      .npmrc
  6. +22
    -0
      LICENSE
  7. +176
    -0
      README.md
  8. BIN
      favicon.ico
  9. +82
    -0
      package.json
  10. +9
    -0
      src/actions/fuelSavingsActions.js
  11. +10
    -0
      src/businessLogic/dateHelper.js
  12. +26
    -0
      src/businessLogic/dateHelper.spec.js
  13. +67
    -0
      src/businessLogic/fuelSavingsCalculator.js
  14. +123
    -0
      src/businessLogic/fuelSavingsCalculator.spec.js
  15. +40
    -0
      src/businessLogic/mathHelper.js
  16. +60
    -0
      src/businessLogic/mathHelper.spec.js
  17. +58
    -0
      src/businessLogic/numberFormatter.js
  18. +38
    -0
      src/businessLogic/numberFormatter.spec.js
  19. +72
    -0
      src/components/FuelSavingsApp.js
  20. +8
    -0
      src/components/FuelSavingsApp.spec.js
  21. +49
    -0
      src/components/FuelSavingsResults.js
  22. +63
    -0
      src/components/FuelSavingsResults.spec.js
  23. +31
    -0
      src/components/FuelSavingsTextInput.js
  24. +2
    -0
      src/constants/ActionTypes.js
  25. +37
    -0
      src/containers/App.js
  26. +11
    -0
      src/index.html
  27. +14
    -0
      src/index.js
  28. +53
    -0
      src/reducers/fuelSavings.js
  29. +8
    -0
      src/reducers/index.js
  30. +25
    -0
      src/store/configureStore.dev.js
  31. +5
    -0
      src/store/configureStore.js
  32. +6
    -0
      src/store/configureStore.prod.js
  33. +31
    -0
      src/styles/styles.scss
  34. +45
    -0
      tools/build.js
  35. +45
    -0
      tools/buildHtml.js
  36. +19
    -0
      tools/distServer.js
  37. +44
    -0
      tools/srcServer.js
  38. +83
    -0
      webpack.config.js

+ 19
- 0
.babelrc View File

@ -0,0 +1,19 @@
{
"presets": ["es2015", "react", "stage-1"],
"env": {
"development": {
"plugins": [
["react-transform", {
"transforms": [{
"transform": "react-transform-hmr",
"imports": ["react"],
"locals": ["module"]
}, {
"transform": "react-transform-catch-errors",
"imports": ["react", "redbox-react"]
}]
}]
]
}
}
}

+ 13
- 0
.editorconfig View File

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

+ 65
- 0
.eslintrc View File

@ -0,0 +1,65 @@
{
"parser": "babel-eslint",
"extends": "eslint:recommended",
"plugins": [
"react"
],
"env": {
"es6": true,
"browser": true,
"node": true,
"jquery": true,
"mocha": true
},
"ecmaFeatures": {
"jsx": true,
"modules": true
},
"rules": {
"quotes": 0,
"no-console": 1,
"no-debugger": 1,
"no-var": 1,
"semi": [1, "always"],
"no-trailing-spaces": 0,
"eol-last": 0,
"no-unused-vars": 0,
"no-underscore-dangle": 0,
"no-alert": 0,
"no-lone-blocks": 0,
"jsx-quotes": 1,
"react/display-name": [ 1, {"ignoreTranspilerName": false }],
"react/forbid-prop-types": [1, {"forbid": ["any"]}],
"react/jsx-boolean-value": 1,
"react/jsx-closing-bracket-location": 0,
"react/jsx-curly-spacing": 1,
"react/jsx-indent-props": 0,
"react/jsx-key": 1,
"react/jsx-max-props-per-line": 0,
"react/jsx-no-bind": 0,
"react/jsx-no-duplicate-props": 1,
"react/jsx-no-literals": 0,
"react/jsx-no-undef": 1,
"react/jsx-pascal-case": 1,
"react/jsx-sort-prop-types": 0,
"react/jsx-sort-props": 0,
"react/jsx-uses-react": 1,
"react/jsx-uses-vars": 1,
"react/no-danger": 1,
"react/no-did-mount-set-state": 1,
"react/no-did-update-set-state": 1,
"react/no-direct-mutation-state": 1,
"react/no-multi-comp": 1,
"react/no-set-state": 1,
"react/no-unknown-property": 1,
"react/prefer-es6-class": 1,
"react/prop-types": 1,
"react/react-in-jsx-scope": 1,
"react/require-extension": 1,
"react/self-closing-comp": 1,
"react/sort-comp": 1,
"react/wrap-multilines": 1
},
"globals": {
}
}

+ 36
- 0
.gitignore View File

@ -0,0 +1,36 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
#dist folder
dist
#Webstorm metadata
.idea
# Mac files
.DS_Store

+ 1
- 0
.npmrc View File

@ -0,0 +1 @@
save-exact=true

+ 22
- 0
LICENSE View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Cory House
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 176
- 0
README.md View File

@ -0,0 +1,176 @@
# React Slingshot!
React Slingshot is a comprehensive starter kit for rapid application development using React. It offers a rich development experience including:
| **Tech** | **Description** |**Learn More**|
|----------|-------|---|
| [React](https://facebook.github.io/react/) | Fast, composable client-side components. | [Pluralsight Course](https://www.pluralsight.com/courses/react-flux-building-applications) |
| [Redux](http://redux.js.org) | Enforces unidirectional data flows and immutable, hot reloadable store. Supports time-travel debugging. Lean alternative to [Facebook's Flux](https://facebook.github.io/flux/docs/overview.html).| [Tutorial](https://egghead.io/series/getting-started-with-redux) |
| [Babel](http://babeljs.io) | Compiles ES6 to ES5. Enjoy the new version of JavaScript today. | [ES6 REPL](https://babeljs.io/repl/), [ES6 vs ES5](http://es6-features.org), [ES6 Katas](http://es6katas.org), [Pluralsight course](https://www.pluralsight.com/courses/javascript-fundamentals-es6) |
| [Webpack](http://webpack.github.io) | Bundles npm packages and our JS into a single file. Includes hot reloading via [react-transform-hmr](https://www.npmjs.com/package/react-transform-hmr). | [Quick Webpack How-to](https://github.com/petehunt/webpack-howto) [Pluralsight Course](https://www.pluralsight.com/courses/webpack-fundamentals)|
| [Browsersync](https://www.browsersync.io/) | Lightweight development HTTP server that supports synchronized testing and debugging on multiple devices. | [Intro vid](https://www.youtube.com/watch?time_continue=1&v=heNWfzc7ufQ)|
| [Mocha](http://mochajs.org) | Automated tests with [Chai](http://chaijs.com/) for assertions and [Cheerio](https://www.npmjs.com/package/cheerio) for DOM testing without a browser using Node. | [Pluralsight Course](https://www.pluralsight.com/courses/testing-javascript) |
| [TrackJS](https://trackjs.com/) | JavaScript error tracking. | [Free trial](https://my.trackjs.com/signup)|
| [ESLint](http://eslint.org/)| Lint JS. Reports syntax and style issues. Using [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react) for additional React specific linting rules. | |
| [SASS](http://sass-lang.com/) | Compiled CSS styles with variables, functions, and more. | [Pluralsight Course](https://www.pluralsight.com/courses/better-css)|
| [Editor Config](http://editorconfig.org) | Enforce consistent editor settings (spaces vs tabs, etc). | [IDE Plugins](http://editorconfig.org/#download) |
| [npm Scripts](https://docs.npmjs.com/misc/scripts)| Glues all this together in a handy automated build. | [Pluralsight course](https://www.pluralsight.com/courses/npm-build-tool-introduction), [Why not Gulp?](https://medium.com/@housecor/why-i-left-gulp-and-grunt-for-npm-scripts-3d6853dd22b8#.vtaziro8n) |
The starter kit includes a working example app that puts all of the above to use.
## Get Started
1. **Initial Machine Setup**. First time running the starter kit? Then complete the [Initial Machine Setup](https://github.com/coryhouse/react-slingshot#initial-machine-setup).
2. **Clone the project**. `git clone https://github.com/coryhouse/react-slingshot.git`.
3. **Install Node packages**. `npm install`
4. **Run the example app**. `npm start -s`
This will run the automated build process, start up a webserver, and open the application in your default browser. When doing development with this kit, you'll want to keep the command line open at all times so that your code is rebuilt and tests run automatically every time you hit save. Note: The -s flag is optional. It enables silent mode which suppresses unnecessary messages during the build.
5. **Review the example app.** This starter kit includes a working example app that calculates fuel savings. Note how all source code is placed under /src. Tests are placed alongside the file under test. The final built app is placed under /dist. These are the files you run in production.
6. **Delete the example app files.** Once you're comfortable with how the example app works, you can [delete those files and begin creating your own app](https://github.com/coryhouse/react-slingshot#i-just-want-an-empty-starter-kit).
##Initial Machine Setup
1. **Install [Node 4.0.0 or greater](https://nodejs.org)**
2. **Install [Git](https://git-scm.com/downloads)**.
3. **Install [React developer tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) and [Redux Dev Tools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en)** in Chrome. (Optional, but helpful. The latter offers time-travel debugging.)
4. On a Mac? You're all set. If you're on Linux or Windows, complete the steps for your OS below.
**On Linux:**
* Run this to [increase the limit](http://stackoverflow.com/questions/16748737/grunt-watch-error-waiting-fatal-error-watch-enospc) on the number of files Linux will watch. [Here's why](https://github.com/coryhouse/react-slingshot/issues/6).
`echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p`
**On Windows:**
* **Install [Python 2.7](https://www.python.org/downloads/)**. Some node modules may rely on node-gyp, which requires Python on Windows.
* **Install C++ Compiler**. Browser-sync requires a C++ compiler on Windows. [Visual Studio Express](https://www.visualstudio.com/en-US/products/visual-studio-express-vs) comes bundled with a free C++ compiler. Or, if you already have Visual Studio installed: Open Visual Studio and go to File -> New -> Project -> Visual C++ -> Install Visual C++ Tools for Windows Desktop. The C++ compiler is used to compile browser-sync (and perhaps other Node modules).
##FAQ
###Why does this exist?
This starter kit implements best practices like testing, minification, bundling, and so on. It codifies a long list of decisions that you no longer have to make to get rolling. It saves you from the long, painful process of wiring it all together into an automated dev environment and build process. It's also useful as inspiration for ideas you might want to integrate into your current development environment or build process.
###What do the scripts in package.json do?
Unfortunately, scripts in package.json can't be commented inline because the JSON spec doesn't support comments, so I'm providing info on what each script in package.json does here.
| **Script** | **Description** |
|----------|-------|
| prestart | Runs automatically before start. Calls remove-dist script which deletes the dist folder. This helps remind you to run the build script before committing since the dist folder will be deleted if you don't. ;) |
| start | Runs tests, lints, starts dev webserver, and opens the app in your default browser. |
| lint:tools | Runs ESLint on build related JS files. (eslint-loader lints src files via webpack when `npm start` is run) |
| clean-dist | Removes everything from the dist folder. |
| remove-dist | Deletes the dist folder. |
| create-dist | Creates the dist folder and the necessary subfolders. |
| build:html | Adds trackJS tracking script and copies to /dist. |
| prebuild | Runs automatically before build script (due to naming convention). Cleans dist folder, builds html, and builds sass. |
| build | Bundles all JavaScript using webpack and writes it to /dist. |
| test | Runs tests (files ending in .spec.js) using Mocha and outputs results to the command line. Watches all files so tests are re-run upon save. |
###Can you explain the file structure?
```
.
├── .babelrc # Configures Babel
├── .editorconfig # Configures editor rules
├── .eslintrc # Configures ESLint
├── .gitignore # Tells git which files to ignore
├── README.md # This file.
├── dist # Folder where the build script places the built app. Use this in prod.
├── package.json # Package configuration. The list of 3rd party libraries and utilities
├── src # Source code
│   ├── actions # Flux/Redux actions. List of distinct actions that can occur in the app.
│   ├── businessLogic # Plain old JS objects (POJOs). Pure logic. No framework specific code here.
│   ├── components # React components
│   ├── constants # Application constants including constants for Redux
│   ├── containers # App container for Redux
│   ├── favicon.ico # favicon to keep your browser from throwing a 404 during dev. Not actually used in prod build.
│   ├── index.html # Start page
│   ├── index.js # Entry point for your app
│   ├── reducers # Redux reducers. Your state is altered here based on actions
│   ├── store # Redux store configuration
│   └── styles # CSS Styles, typically written in Sass
├── tools # Node scripts that run build related tools
│   ├── build.js # Runs the production build
│   ├── buildHtml.js # Builds index.html
│   ├── distServer.js # Starts webserver and opens final built app that's in dist in your default browser
│   ├── srcServer.js # Starts dev webserver with hot reloading and opens your app in your default browser
└── webpack.config.js # Configures webpack
```
###Where are the files being served from when I run `npm start`?
Webpack serves your app in memory when you run `npm start`. No physical files are written. However, the web root is /src, so you can reference files under /src in index.html. When the app is built using `npm run build`, physical files are written to /dist and the app is served from /dist.
###How is Sass being converted into CSS and landing in the browser?
Magic! Okay, more specifically, we're handling it differently in dev (`npm start`) vs prod (`npm run build`)
When you run `npm start`:
1. The sass-loader compiles Sass into CSS
2. Webpack bundles the compiled CSS into bundle.js. Sounds odd, but it works!
3. bundle.js contains code that loads styles into the <head> of index.html via JavaScript. This is why you don't see a stylesheet reference in index.html. In fact, if you disable JavaScript in your browser, you'll see the styles don't load either.
The approach above supports hot reloading, which is great for development. However, it also create a flash of unstyled content on load because you have to wait for the JavaScript to parse and load styles before they're applied. So for the production build, we use a different approach:
When you run `npm run build`:
1. The sass-loader compiles Sass into CSS
2. The [extract-text-webpack-plugin](https://github.com/webpack/extract-text-webpack-plugin) extracts the compiled Sass into styles.css
3. buildHtml.js adds a reference to the stylesheet to the head of index.html.
For both of the above methods, a separate sourcemap is generated for debugging Sass in [compatible browsers](http://thesassway.com/intermediate/using-source-maps-with-sass).
###I don't like the magic you just described above. I simply want to use a CSS file.
No problem. Reference your CSS file in index.html, and add a step to the build process to copy your CSS file over to the same relative location /dist as part of the build step. But be forwarned, you lose style hot reloading with this approach.
### I just want an empty starter kit.
This starter kit includes an example app so you can see how everything hangs together on a real app. To create an empty project, you can delete the following:
1. Components in src/components
2. Styles in src/styles/styles.scss
3. Delete files in src/businessLogic
Don't want to use Redux? See the next question for some steps on removing Redux.
### Do I have to use Redux?
Nope. Redux is useful for applications with more complex data flows. If your app is simple, Redux is overkill. Remove Redux like this:
1. Delete the following folders and their contents: actions, constants, reducers, containers, store
2. Uninstall Redux related packages: `npm uninstall redux react-redux redux-thunk`
3. Remove Redux related imports from /src/index.js: `import configureStore from './store/configureStore';`, `import App from './containers/App';` and `import { Provider } from 'react-redux';`
4. Remove this line from /src/index.js: `const store = configureStore();`
5. Delete components in /components and create a new empty component.
6. Replace the call to `<Provider><App/></Provider>` in /src/index.js with a call to the new top level component you just created in step 5.
### How do I deploy this?
`npm run build`. This will build the project for production. It does the following:
* Minifies all JS
* Sets NODE_ENV to prod so that React is built in production mode
* Places the resulting built project files into /dist. (This is the folder you'll expose to the world).
### Why are test files placed alongside the file under test (instead of centralized)?
Streamlined automated testing is a core feature of this starter kit. All tests are placed in files that end in .spec.js. Spec files are placed in the same directory as the file under test. Why?
+ The existence of tests is highly visible. If a corresponding .spec file hasn't been created, it's obvious.
+ Easy to open since they're in the same folder as the file being tested.
+ Easy to create new test files when creating new source files.
+ Short import paths are easy to type and less brittle.
+ As files are moved, it's easy to move tests alongside.
That said, you can of course place your tests under /test instead, which is the Mocha default. If you do, you can simplify the test script to no longer specify the path. Then Mocha will simply look in /test to find your spec files.
### How do I debug?
Since browsers don't currently support ES6, we're using Babel to compile our ES6 down to ES5. This means the code that runs in the browser looks different than what we wrote. But good news, a [sourcemap](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/) is generated to enable easy debugging. This means your original JS source will be displayed in your browser's dev console.
*Note:* When you run `npm start`, no JS is minified. Why? Because minifying slows the build. So JS is only minified when you run the `npm run build` script. See [more on building for production below](https://github.com/coryhouse/react-slingshot#how-do-i-deploy-this).
Also note that no actual physical files are written to the filesystem during the dev build. **For performance, all files exist in memory when served from the webpack server.**. Physical files are only written when you run `npm run build`.
**Tips for debugging via sourcemaps:**
1. Browsers vary in the way they allow you to view the original source. Chrome automatically shows the original source if a sourcemap is available. Safari, in contrast, will display the minified source and you'll [have to cmd+click on a given line to be taken to the original source](http://stackoverflow.com/questions/19550060/how-do-i-toggle-source-mapping-in-safari-7).
2. Do **not** enable serving files from your filesystem in Chrome dev tools. If you do, Chrome (and perhaps other browsers) may not show you the latest version of your code after you make a source code change. Instead **you must close the source view tab you were using and reopen it to see the updated source code**. It appears Chrome clings to the old sourcemap until you close and reopen the source view tab. To clarify, you don't have to close the actual tab that is displaying the app, just the tab in the console that's displaying the source file that you just changed.
3. If the latest source isn't displaying the console, force a refresh. Sometimes Chrome seems to hold onto a previous version of the sourcemap which will cause you to see stale code.
### I'm getting an error when running npm install: Failed to locate "CL.exe"
On Windows, you need to install extra dependencies for browser-sync to build and install successfully. Follow the getting started steps above to assure you have the necessary dependencies on your machine.
### I can't access the external URL for Browsersync
To hit the external URL, all devices must be on the same LAN. So this may mean your dev machine needs to be on the same Wifi as the mobile devices you're testing.
###What about the Redux Devtools?
They're not included at this time to keep the project simple. If you're interested, Barry Staes created a [branch with the devtools incorporated](https://github.com/coryhouse/react-slingshot/pull/27).

BIN
favicon.ico View File

Before After

+ 82
- 0
package.json View File

@ -0,0 +1,82 @@
{
"name": "react-slingshot",
"version": "1.2.0",
"description": "Starter kit for creating apps with React and Redux",
"scripts": {
"prestart": "npm run remove-dist",
"start": "parallelshell \"npm run lint:tools\" \"npm run test:watch\" \"npm run open:src\"",
"open:src": "babel-node tools/srcServer.js",
"open:dist": "babel-node tools/distServer.js",
"lint:tools": "eslint webpack.config.js tools",
"clean-dist": "npm run remove-dist && mkdir dist",
"remove-dist": "node_modules/.bin/rimraf ./dist",
"build:html": "babel-node tools/buildHtml.js",
"prebuild": "npm run clean-dist && npm run build:html",
"build": "npm run test && babel-node tools/build.js && npm run open:dist",
"test": "cross-env NODE_ENV=test mocha --reporter progress --compilers js:babel-core/register --recursive \"./src/**/*.spec.js\"",
"test:watch": "npm run test -- --watch"
},
"author": "Cory House",
"license": "MIT",
"dependencies": {
"object-assign": "4.0.1",
"react": "0.14.7",
"react-dom": "0.14.7",
"react-redux": "4.4.0",
"redux": "3.3.1"
},
"devDependencies": {
"babel-cli": "6.5.1",
"babel-core": "6.5.1",
"babel-eslint": "5.0.0",
"babel-loader": "6.2.3",
"babel-plugin-react-display-name": "2.0.0",
"babel-plugin-react-transform": "2.0.0",
"babel-preset-es2015": "6.5.0",
"babel-preset-react": "6.5.0",
"babel-preset-stage-1": "6.5.0",
"browser-sync": "2.11.1",
"chai": "3.4.1",
"cheerio": "0.19.0",
"colors": "1.1.2",
"cross-env": "1.0.7",
"css-loader": "0.23.1",
"eslint": "2.2.0",
"eslint-loader": "1.3.0",
"eslint-plugin-react": "4.0.0",
"extract-text-webpack-plugin": "1.0.1",
"file-loader": "0.8.5",
"mocha": "2.3.4",
"node-sass": "3.4.2",
"parallelshell": "2.0.0",
"react-transform-catch-errors": "1.0.1",
"react-transform-hmr": "1.0.1",
"redbox-react": "1.2.0",
"rimraf": "2.5.0",
"sass-loader": "3.1.2",
"style-loader": "0.13.0",
"watch": "0.17.1",
"webpack": "1.12.11",
"webpack-dev-middleware": "1.4.0",
"webpack-hot-middleware": "2.6.0",
"yargs": "3.32.0"
},
"keywords:": [
"react",
"reactjs",
"hot",
"reload",
"hmr",
"live",
"edit",
"webpack",
"redux",
"flux",
"boilerplate",
"starter"
],
"repository": {
"type": "git",
"url": "https://github.com/coryhouse/react-slingshot"
}
}

+ 9
- 0
src/actions/fuelSavingsActions.js View File

@ -0,0 +1,9 @@
import * as types from '../constants/ActionTypes';
export function saveFuelSavings(settings) {
return { type: types.SAVE_FUEL_SAVINGS, settings };
}
export function calculateFuelSavings(settings, fieldName, value) {
return { type: types.CALCULATE_FUEL_SAVINGS, settings, fieldName, value };
}

+ 10
- 0
src/businessLogic/dateHelper.js View File

@ -0,0 +1,10 @@
export default class DateHelper {
//See tests for desired format.
static getFormattedDateTime(date) {
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${this.padLeadingZero(date.getMinutes())}:${this.padLeadingZero(date.getSeconds())}`;
}
static padLeadingZero(value) {
return value > 9 ? value : '0' + value;
}
}

+ 26
- 0
src/businessLogic/dateHelper.spec.js View File

@ -0,0 +1,26 @@
import chai from 'chai';
import DateHelper from './dateHelper';
chai.should();
describe('Date Helper', () => {
describe('getFormattedDateTime', () => {
it('returns mm/dd hh:mm:ss formatted time when passed a date', () => {
//arrange
//The 7 numbers specify the year, month, day, hour, minute, second, and millisecond, in that order
let date = new Date(99,0,24,11,33,30,0);
//assert
DateHelper.getFormattedDateTime(date).should.equal('1/24 11:33:30');
});
it('pads single digit minute and second values with leading zeros', () => {
//arrange
//The 7 numbers specify the year, month, day, hour, minute, second, and millisecond, in that order
let date = new Date(99,0,4,11,3,2,0);
//assert
DateHelper.getFormattedDateTime(date).should.equal('1/4 11:03:02');
});
});
});

+ 67
- 0
src/businessLogic/fuelSavingsCalculator.js View File

@ -0,0 +1,67 @@
import mathHelper from './mathHelper';
import NumberFormatter from './numberFormatter';
//This file uses the factory function pattern instead of a class.
//Just showing an alternative to using a class.
//This declares a function with a private method.
//The public function returns an object literal.
//Could arguably be called FuelSavingCalculatorFactory.
let fuelSavingsCalculator = function() {
//private
let calculateMonthlyCost = function(milesDrivenPerMonth, ppg, mpg) {
let gallonsUsedPerMonth = milesDrivenPerMonth / mpg;
return gallonsUsedPerMonth * ppg;
};
//public
return {
calculateMilesDrivenPerMonth: function(milesDriven, milesDrivenTimeframe) {
const monthsPerYear = 12;
const weeksPerYear = 52;
switch (milesDrivenTimeframe) {
case 'week':
return (milesDriven * weeksPerYear) / monthsPerYear;
case 'month':
return milesDriven;
case 'year':
return milesDriven / monthsPerYear;
default:
throw 'Unknown milesDrivenTimeframe passed: ' + milesDrivenTimeframe;
}
},
calculateSavingsPerMonth: function(settings) {
if (!settings.milesDriven) {
return 0;
}
let milesDrivenPerMonth = this.calculateMilesDrivenPerMonth(settings.milesDriven, settings.milesDrivenTimeframe);
let tradeFuelCostPerMonth = calculateMonthlyCost(milesDrivenPerMonth, settings.tradePpg, settings.tradeMpg);
let newFuelCostPerMonth = calculateMonthlyCost(milesDrivenPerMonth, settings.newPpg, settings.newMpg);
let savingsPerMonth = tradeFuelCostPerMonth - newFuelCostPerMonth;
return mathHelper.roundNumber(savingsPerMonth, 2);
},
necessaryDataIsProvidedToCalculateSavings: function(settings) {
return settings.newMpg > 0
&& settings.tradeMpg > 0
&& settings.newPpg > 0
&& settings.tradePpg > 0
&& settings.milesDriven > 0;
},
calculateSavings: function(settings) {
let monthlySavings = this.calculateSavingsPerMonth(settings);
return {
monthly: NumberFormatter.getCurrencyFormattedNumber(monthlySavings),
annual: NumberFormatter.getCurrencyFormattedNumber(monthlySavings * 12),
threeYear: NumberFormatter.getCurrencyFormattedNumber(monthlySavings * 12 * 3)
};
}
};
};
export default fuelSavingsCalculator;

+ 123
- 0
src/businessLogic/fuelSavingsCalculator.spec.js View File

@ -0,0 +1,123 @@
import chai from 'chai';
import Calculator from './fuelSavingsCalculator';
chai.should();
describe('Fuel Savings Calculator', () => {
describe('necessaryDataIsProvidedToCalculateSavings', () => {
it('returns false when necessary data isn\'t provided', () => {
//arrange
let settings = {
newMpg: 20
};
//assert
Calculator().necessaryDataIsProvidedToCalculateSavings(settings).should.equal(false);
});
it('returns true when necessary data is provided', () => {
//arrange
let settings = {
newMpg: 20,
tradeMpg: 10,
newPpg: 1.50,
tradePpg: 1.50,
milesDriven: 100
};
//assert
Calculator().necessaryDataIsProvidedToCalculateSavings(settings).should.equal(true);
});
});
describe("milesPerMonth", () => {
it("converts a weekly timeframe to a monthly timeframe", () => {
//arrange
var milesPerWeek = 100;
//act
var milesPerMonth = Calculator().calculateMilesDrivenPerMonth(milesPerWeek, 'week');
//assert
milesPerMonth.should.equal(433.3333333333333);
});
it("returns a monthly timeframe untouched", () => {
//arrange
var milesPerMonth = 300;
//act
var milesPerMonthCalculated = Calculator().calculateMilesDrivenPerMonth(milesPerMonth, 'month');
//assert
milesPerMonthCalculated.should.equal(milesPerMonth);
});
it("converts a yearly timeframe to a monthly timeframe", () => {
//arrange
var milesPerYear = 1200;
//act
var milesPerMonth = Calculator().calculateMilesDrivenPerMonth(milesPerYear, 'year');
//assert
milesPerMonth.should.equal(100);
});
});
describe("calculateSavingsPerMonth", () => {
it("returns 29.93 in savings per month with these settings", () => {
//arrange
var settings = {
tradePpg: 3.75,
tradeMpg: 24,
newPpg: 3.75,
newMpg: 38,
milesDriven: 120,
milesDrivenTimeframe: 'week'
};
//act
var savingsPerMonth = Calculator().calculateSavingsPerMonth(settings);
//assert
savingsPerMonth.should.equal(29.93);
});
it("returns 40.83 in savings per month with these settings", () => {
//arrange
var settings = {
tradePpg: 4.15,
tradeMpg: 24,
newPpg: 3.75,
newMpg: 38,
milesDriven: 550,
milesDrivenTimeframe: 'month'
};
//act
var savingsPerMonth = Calculator().calculateSavingsPerMonth(settings);
//assert
savingsPerMonth.should.equal(40.83);
});
it("returns -157.12 in loss per month with these settings", () => {
//arrange
var settings = {
tradePpg: 3.15,
tradeMpg: 40,
newPpg: 3.75,
newMpg: 18,
milesDriven: 14550,
milesDrivenTimeframe: 'year'
};
//act
var savingsPerMonth = Calculator().calculateSavingsPerMonth(settings);
//assert
savingsPerMonth.should.equal(-157.12);
});
});
});

+ 40
- 0
src/businessLogic/mathHelper.js View File

@ -0,0 +1,40 @@
class MathHelper {
static roundNumber(numberToRound, numberOfDecimalPlaces) {
if (numberToRound === 0) {
return 0;
}
if (!numberToRound) {
return '';
}
const scrubbedNumber = numberToRound.toString().replace('$', '').replace(',', '');
return Math.round(scrubbedNumber * Math.pow(10, numberOfDecimalPlaces)) / Math.pow(10, numberOfDecimalPlaces);
}
static addArray(values) { //adds array of values passed.
if (values == null) {
return null;
}
let total = 0;
for (let i in values) {
total += parseInt(this.convertToPennies(values[i])); //do math in pennies to assure accuracy.
}
return total / 100; //convert back into dollars
}
static convertToPennies(dollarValue) {
if (dollarValue === 0) {
return 0;
}
dollarValue = parseFloat(dollarValue);
dollarValue = this.roundNumber(dollarValue, 2); //round to 2 decimal places.
const dollarValueContainsDecimal = (dollarValue.toString().indexOf(".") !== -1);
return (dollarValueContainsDecimal) ? parseInt(dollarValue.toString().replace('.', '')) : parseInt(dollarValue) * 100;
}
}
export default MathHelper;

+ 60
- 0
src/businessLogic/mathHelper.spec.js View File

@ -0,0 +1,60 @@
import chai from 'chai';
import MathHelper from './mathHelper';
chai.should();
describe('Math Helper', () => {
describe('roundNumber', () => {
it('returns 0 when passed null', () => {
MathHelper.roundNumber(null).should.equal('');
});
it('returns 0 when passed 0', () => {
MathHelper.roundNumber(0).should.equal(0);
});
it('rounds up to 1.56 when passed 1.55555 rounded to 2 digits', () => {
MathHelper.roundNumber(1.55555, 2).should.equal(1.56);
});
it('rounds up to -1.56 when passed -1.55555 rounded to 2 digits', () => {
MathHelper.roundNumber(-1.55555, 2).should.equal(-1.56);
});
});
describe('addArray', () => {
it('returns 0 when passed empty array', () => {
MathHelper.addArray([]).should.equal(0);
});
// it('returns null when passed null', () => {
// should.not.exist(MathHelper.addArray(null));
// });
it('returns 6 when passed [2,4]', () => {
MathHelper.addArray([2,4]).should.equal(6);
});
it('returns 7 when passed [-6, 11, 2]', () => {
MathHelper.addArray([-6, 11, 2]).should.equal(7);
});
});
describe('convertToPennies', () => {
it('returns 142 when passed 1.42', () => {
MathHelper.convertToPennies(1.42).should.equal(142);
});
it('returns 0 when passed 0', () => {
MathHelper.convertToPennies(0).should.equal(0);
});
it('rounds down to 132 when passed 1.3242', () => {
MathHelper.convertToPennies(1.3242).should.equal(132);
});
it('rounds up to 133 when passed 1.325', () => {
MathHelper.convertToPennies(1.325).should.equal(133);
});
});
});

+ 58
- 0
src/businessLogic/numberFormatter.js View File

@ -0,0 +1,58 @@
import MathHelper from './mathHelper';
class NumberFormatter {
static getCurrencyFormattedNumber(value) {
if (value === null) {
return '';
}
value = this.getFormattedNumber(value);
return '$' + value;
}
static getFormattedNumber(value) {
if (value === 0) {
return 0;
}
if (!value) {
return '';
}
if (!this.isInt(this.scrubFormatting(value))) {
return ''; //if it's not a number after scrubbing formatting, just return empty.
}
let roundedValue = MathHelper.roundNumber(value, 2); //round if more than 2 decimal points
roundedValue = roundedValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); //add commas for 1,000's. RegEx from http://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
const roundedValueContainsDecimalPlace = (roundedValue.indexOf('.') !== -1);
if (roundedValueContainsDecimalPlace) {
const numbersToTheRightOfDecimal = roundedValue.split('.')[1];
switch (numbersToTheRightOfDecimal.length) {
case 0:
return roundedValue.replace('.', ''); //no decimal necessary since no numbers after decimal
case 1:
return roundedValue + '0';
default:
return roundedValue;
}
}
return roundedValue;
}
static isInt(n) {
if (n === '' || n === null) {
return false;
}
return n % 1 === 0;
}
static scrubFormatting(value) {
return value.toString().replace('$', '').replace(',', '').replace('.', '');
}
}
export default NumberFormatter;

+ 38
- 0
src/businessLogic/numberFormatter.spec.js View File

@ -0,0 +1,38 @@
import NumberFormatter from './numberFormatter';
import chai from 'chai';
chai.should();
describe('Number Formatter', () => {
describe('getCurrencyFormattedNumber', () => {
it('returns $5.50 when passed 5.5', () => {
NumberFormatter.getCurrencyFormattedNumber(5.5).should.equal("$5.50");
});
});
describe('isInt', () => {
it('returns true when passed 0', () => {
NumberFormatter.isInt(0).should.equal(true);
});
it('returns false when passed an empty string', () => {
NumberFormatter.isInt('').should.equal(false);
});
it('returns true when passed int as a string', () => {
NumberFormatter.isInt('5').should.equal(true);
});
});
describe('scrubFormatting', () => {
it('strips commas and decimals', () => {
NumberFormatter.scrubFormatting('1,234.56').should.equal('123456');
});
});
describe('getFormattedNumber', () => {
it('returns 0 if passed 0', () => {
NumberFormatter.getFormattedNumber(0).should.equal(0);
});
});
});

+ 72
- 0
src/components/FuelSavingsApp.js View File

@ -0,0 +1,72 @@
import React, {PropTypes} from 'react';
import FuelSavingsResults from './FuelSavingsResults';
import FuelSavingsTextInput from './FuelSavingsTextInput';
const FuelSavingsApp = (props) => {
const save = function () {
props.actions.saveFuelSavings(props.appState);
};
const onTimeframeChange = function (e) {
props.actions.calculateFuelSavings(props, 'milesDrivenTimeframe', e.target.value);
};
const fuelSavingsKeypress = function (name, value) {
props.actions.calculateFuelSavings(props, name, value);
};
const settings = props.appState;
return (
<div>
<h2>Fuel Savings Analysis</h2>
<table>
<tbody>
<tr>
<td><label htmlFor="newMpg">New Vehicle MPG</label></td>
<td><FuelSavingsTextInput onChange={fuelSavingsKeypress} name="newMpg" value={settings.newMpg} /></td>
</tr>
<tr>
<td><label htmlFor="tradeMpg">Trade-in MPG</label></td>
<td><FuelSavingsTextInput onChange={fuelSavingsKeypress} name="tradeMpg" value={settings.tradeMpg} /></td>
</tr>
<tr>
<td><label htmlFor="newPpg">New Vehicle price per gallon</label></td>
<td><FuelSavingsTextInput onChange={fuelSavingsKeypress} name="newPpg" value={settings.newPpg} /></td>
</tr>
<tr>
<td><label htmlFor="tradePpg">Trade-in price per gallon</label></td>
<td><FuelSavingsTextInput onChange={fuelSavingsKeypress} name="tradePpg" value={settings.tradePpg} /></td>
</tr>
<tr>
<td><label htmlFor="milesDriven">Miles Driven</label></td>
<td>
<FuelSavingsTextInput onChange={fuelSavingsKeypress} name="milesDriven" value={settings.milesDriven} /> miles per
<select name="milesDrivenTimeframe" onChange={onTimeframeChange} value={settings.milesDrivenTimeframe}>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
</td>
</tr>
<tr>
<td><label>Date Modified</label></td>
<td>{settings.dateModified}</td>
</tr>
</tbody>
</table>
<hr/>
{settings.necessaryDataIsProvidedToCalculateSavings ? <FuelSavingsResults savings={settings.savings} /> : null}
<input type="submit" value="Save" onClick={save} />
</div>
);
};
FuelSavingsApp.propTypes = {
actions: PropTypes.object.isRequired,
appState: PropTypes.object.isRequired
};
export default FuelSavingsApp;

+ 8
- 0
src/components/FuelSavingsApp.spec.js View File

@ -0,0 +1,8 @@
import chai from 'chai';
import FuelSavingsCalculatorForm from './FuelSavingsApp';
chai.should();
describe('Fuel Savings Calculator Component', () => {
});

+ 49
- 0
src/components/FuelSavingsResults.js View File

@ -0,0 +1,49 @@
import React, {PropTypes} from 'react';
import NumberFormatter from '../businessLogic/numberFormatter';
//This is a stateless functional component. (Also known as pure or dumb component)
//More info: https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html#stateless-functional-components
//And https://medium.com/@joshblack/stateless-components-in-react-0-14-f9798f8b992d
//And starter kit with more examples here: https://github.com/ericelliott/react-pure-component-starter
const FuelSavingsResults = (props) => {
const savingsExist = NumberFormatter.scrubFormatting(props.savings.monthly) > 0;
const savingsClass = savingsExist ? 'savings' : 'loss';
const resultLabel = savingsExist ? 'Savings' : 'Loss';
//You can even exclude the return statement below if the entire component is
//composed within the parentheses. Return is necessary here because some
//variables are set above.
return (
<table>
<tbody>
<tr>
<td className="fuel-savings-label">{resultLabel}</td>
<td>
<table>
<tbody>
<tr>
<td>Monthly</td>
<td>1 Year</td>
<td>3 Year</td>
</tr>
<tr>
<td className={savingsClass}>{props.savings.monthly}</td>
<td className={savingsClass}>{props.savings.annual}</td>
<td className={savingsClass}>{props.savings.threeYear}</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
);
};
//Note that this odd style is utilized for propType validation for now. Must be defined *after*
//the component is defined, which is why it's separate and down here.
FuelSavingsResults.propTypes = {
savings: PropTypes.object.isRequired
};
export default FuelSavingsResults;

+ 63
- 0
src/components/FuelSavingsResults.spec.js View File

@ -0,0 +1,63 @@
import chai from 'chai';
import cheerio from 'cheerio';
import FuelSavingsResults from './FuelSavingsResults';
import React from 'react';
import ReactDOMServer from 'react/lib/ReactDOMServer';
chai.should();
/*This test file displays how to test a React component's HTML
outside of the browser. It uses Cheerio, which is a handy
server-side library that mimics jQuery. So to test a React
components HTML for a given state we do the following:
1. Instantiate the component and pass the desired prop values
2. Use ReactDOMServer to generate the resulting HTML
3. Use Cheerio to load the HTML into a fake DOM
4. Use Cheerio to query the DOM using jQuery style selectors
5. Assert that certain DOM elements exist with expected values.
*/
describe('Fuel Savings Calculator Results Component', () => {
describe('Savings label', () => {
it('displays as savings when savings exist', () => {
//arrange
var props = {
savings: {
monthly: '10',
annual: '120',
threeYear: '360'
}
};
var sut = React.createElement(FuelSavingsResults, props);
//act
var html = ReactDOMServer.renderToStaticMarkup(sut);
let $ = cheerio.load(html);
var fuelSavingsLabel = $('.fuel-savings-label').html();
//assert
fuelSavingsLabel.should.equal('Savings');
});
it('display as loss when savings don\'t exist', () => {
//arrange
var props = {
savings: {
monthly: '-10',
annual: '-120',
threeYear: '-360'
}
};
var sut = React.createElement(FuelSavingsResults, props);
//act
var html = ReactDOMServer.renderToStaticMarkup(sut);
let $ = cheerio.load(html);
var fuelSavingsLabel = $('.fuel-savings-label').html();
//assert
fuelSavingsLabel.should.equal('Loss');
});
});
});

+ 31
- 0
src/components/FuelSavingsTextInput.js View File

@ -0,0 +1,31 @@
import React, { Component, PropTypes } from 'react';
function buildHandleChange(props) {
return function handleChange(e) {
props.onChange(props.name, e.target.value);
};
}
const FuelSavingsTextInput = (props) => {
const handleChange = buildHandleChange(props);
return (
<input className="small"
type="text"
placeholder={props.placeholder}
value={props.value}
onChange={handleChange} />
);
};
FuelSavingsTextInput.propTypes = {
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
])
};
export default FuelSavingsTextInput;

+ 2
- 0
src/constants/ActionTypes.js View File

@ -0,0 +1,2 @@
export const SAVE_FUEL_SAVINGS = 'SAVE_FUEL_SAVINGS';
export const CALCULATE_FUEL_SAVINGS = 'CALCULATE_FUEL_SAVINGS';

+ 37
- 0
src/containers/App.js View File

@ -0,0 +1,37 @@
// This file bootstraps the app with the boilerplate necessary
// to support hot reloading in Redux
import React, {PropTypes} from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import FuelSavingsApp from '../components/FuelSavingsApp';
import * as actions from '../actions/fuelSavingsActions';
class App extends React.Component {
render() {
return (
<FuelSavingsApp appState={this.props.appState} actions={this.props.actions}/>
);
}
}
App.propTypes = {
actions: PropTypes.object.isRequired,
appState: PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
appState: state.fuelSavingsAppState
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);

+ 11
- 0
src/index.html View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>React Slingshot</title>
</head>
<body>
<div id="app"></div>
<script src="bundle.js"></script>
</body>
</html>

+ 14
- 0
src/index.js View File

@ -0,0 +1,14 @@
import React from 'react';
import {render} from 'react-dom';
import { Provider } from 'react-redux';
import App from './containers/App';
import configureStore from './store/configureStore';
import './styles/styles.scss'; //Yep, that's right. You can import SASS/CSS files too! Webpack will run the associated loader and plug this into the page.
const store = configureStore();
render(
<Provider store={store}>
<App />
</Provider>, document.getElementById('app')
);

+ 53
- 0
src/reducers/fuelSavings.js View File

@ -0,0 +1,53 @@
import {SAVE_FUEL_SAVINGS, CALCULATE_FUEL_SAVINGS} from '../constants/ActionTypes';
import calculator from '../businessLogic/fuelSavingsCalculator';
import dateHelper from '../businessLogic/dateHelper';
import objectAssign from 'object-assign';
const initialState = {
newMpg: null,
tradeMpg: null,
newPpg: null,
tradePpg: null,
milesDriven: null,
milesDrivenTimeframe: 'week',
displayResults: false,
dateModified: null,
necessaryDataIsProvidedToCalculateSavings: false,
savings: {
monthly: 0,
annual: 0,
threeYear: 0
}
};
//IMPORTANT: Note that with Redux, state should NEVER be changed.
//State is considered immutable. Instead,
//create a copy of the state passed and set new values on the copy.
//Note that I'm using Object.assign to create a copy of current state
//and update values on the copy.
export default function fuelSavingsAppState(state = initialState, action) {
switch (action.type) {
case SAVE_FUEL_SAVINGS:
// For this example, just simulating a save by changing date modified.
// In a real app using Redux, you might use redux-thunk and handle the async call in fuelSavingsActions.js
return objectAssign({}, state, { dateModified: dateHelper.getFormattedDateTime(new Date()) });
case CALCULATE_FUEL_SAVINGS:
{ // limit scope with this code block, to satisfy eslint no-case-declarations rule.
let newState = objectAssign({}, state);
newState[action.fieldName] = action.value;
let calc = calculator();
newState.necessaryDataIsProvidedToCalculateSavings = calc.necessaryDataIsProvidedToCalculateSavings(newState);
newState.dateModified = dateHelper.getFormattedDateTime(new Date());
if (newState.necessaryDataIsProvidedToCalculateSavings) {
newState.savings = calc.calculateSavings(newState);
}
return newState;
}
default:
return state;
}
}

+ 8
- 0
src/reducers/index.js View File

@ -0,0 +1,8 @@
import { combineReducers } from 'redux';
import fuelSavingsAppState from './fuelSavings';
const rootReducer = combineReducers({
fuelSavingsAppState
});
export default rootReducer;

+ 25
- 0
src/store/configureStore.dev.js View File

@ -0,0 +1,25 @@
//This file merely configures the store for hot reloading.
//This boilerplate file is likely to be the same for each project that uses Redux.
//With Redux, the actual stores are in /reducers.
import { createStore } from 'redux';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
let store;
if (window.devToolsExtension) { //Enable Redux devtools if the extension is installed in developer's browser
store = window.devToolsExtension()(createStore)(rootReducer, initialState);
} else {
store = createStore(rootReducer, initialState);
}
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextReducer = require('../reducers');
store.replaceReducer(nextReducer);
});
}
return store;
}

+ 5
- 0
src/store/configureStore.js View File

@ -0,0 +1,5 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./configureStore.prod')
} else {
module.exports = require('./configureStore.dev')
}

+ 6
- 0
src/store/configureStore.prod.js View File

@ -0,0 +1,6 @@
import { createStore } from 'redux';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(rootReducer, initialState);
}

+ 31
- 0
src/styles/styles.scss View File

@ -0,0 +1,31 @@
/* Variables */
$vin-blue: #5bb7db;
$vin-green: #60b044;
$vin-red: #ff0000;
/* Styles */
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
td {
padding: 12px;
}
h2 {
color: $vin-blue;
}
.savings { color: $vin-green; }
.loss { color: $vin-red; }
input.small { width: 46px; }
td.fuel-savings-label { width: 175px; }

+ 45
- 0
tools/build.js View File

@ -0,0 +1,45 @@
// More info on Webpack's Node API here: https://webpack.github.io/docs/node.js-api.html
// Allowing console calls below since this is a build file.
/*eslint-disable no-console */
import webpack from 'webpack';
import webpackConfigBuilder from '../webpack.config';
import colors from 'colors';
import { argv as args } from 'yargs';
process.env.NODE_ENV = 'production'; // this assures React is built in prod mode and that the Babel dev config doesn't apply.
const webpackConfig = webpackConfigBuilder(process.env.NODE_ENV);
webpack(webpackConfig).run((err, stats) => {
const inSilentMode = args.s; // set to true when -s is passed on the command
if (!inSilentMode) {
console.log('Generating minified bundle for production use via Webpack...'.bold.blue);
}
if (err) { // so a fatal error occurred. Stop here.
console.log(err.bold.red);
return 1;
}
const jsonStats = stats.toJson();
if (jsonStats.hasErrors) {
return jsonStats.errors.map(error => console.log(error.red));
}
if (jsonStats.hasWarnings && !inSilentMode) {
console.log('Webpack generated the following warnings: '.bold.yellow);
jsonStats.warnings.map(warning => console.log(warning.yellow));
}
if (!inSilentMode) {
console.log(`Webpack stats: ${stats}`);
}
// if we got this far, the build succeeded.
console.log('Your app has been compiled in production mode and written to /dist. It\'s ready to roll!'.green.bold);
return 0;
});

+ 45
- 0
tools/buildHtml.js View File

@ -0,0 +1,45 @@
// This script copies src/index.html into /dist/index.html
// and adds TrackJS error tracking code for use in production
// when useTrackJs is set to true below and a trackJsToken is provided.
// This is a good example of using Node and cheerio to do a simple file transformation.
// In this case, the transformation is useful since we only want to track errors in the built production code.
// Allowing console calls below since this is a build file.
/*eslint-disable no-console */
import fs from 'fs';
import colors from 'colors';
import cheerio from 'cheerio';
const useTrackJs = true; // If you choose not to use TrackJS, just set this to false and the build warning will go away.
const trackJsToken = ''; // If you choose to use TrackJS, insert your unique token here. To get a token, go to https://trackjs.com
fs.readFile('src/index.html', 'utf8', (err, markup) => {
if (err) {
return console.log(err);
}
const $ = cheerio.load(markup);
// since a separate spreadsheet is only utilized for the production build, need to dynamically add this here.
$('head').prepend('<link rel="stylesheet" href="styles.css">');
if (useTrackJs) {
if (trackJsToken) {
const trackJsCode = `<!-- BEGIN TRACKJS Note: This should be the first <script> on the page per https://my.trackjs.com/install --><script>window._trackJs = { token: '${trackJsToken}' };</script><script src=https://d2zah9y47r7bi2.cloudfront.net/releases/current/tracker.js></script><!-- END TRACKJS -->`;
$('head').prepend(trackJsCode); // add TrackJS tracking code to the top of <head>
} else {
console.log('To track JavaScript errors, sign up for a free trial at TrackJS.com and enter your token in /tools/build.html on line 10.'.yellow);
}
}
fs.writeFile('dist/index.html', $.html(), 'utf8', function (err) {
if (err) {
return console.log(err);
}
});
console.log('index.html written to /dist'.green);
});

+ 19
- 0
tools/distServer.js View File

@ -0,0 +1,19 @@
// This file configures a web server for testing the production build
// on your local machine.
import browserSync from 'browser-sync';
// Run Browsersync
browserSync({
port: 3000,
ui: {
port: 3001
},
server: {
baseDir: 'dist'
},
files: [
'src/*.html'
]
});

+ 44
- 0
tools/srcServer.js View File

@ -0,0 +1,44 @@
// This file configures the development web server
// which supports hot reloading and synchronized testing.
// Require Browsersync along with webpack and middleware for it
import browserSync from 'browser-sync';
import webpack from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpackConfigBuilder from '../webpack.config';
const webpackConfig = webpackConfigBuilder('development');
const bundler = webpack(webpackConfig);
// Run Browsersync and use middleware for Hot Module Replacement
browserSync({
server: {
baseDir: 'src',
middleware: [
webpackDevMiddleware(bundler, {
// Dev middleware can't access config, so we provide publicPath
publicPath: webpackConfig.output.publicPath,
// pretty colored output
stats: { colors: true },
// Set to false to display a list of each file that is being bundled.
noInfo: true
// for other settings see
// http://webpack.github.io/docs/webpack-dev-middleware.html
}),
// bundler should be the same as above
webpackHotMiddleware(bundler)
]
},
// no need to watch '*.js' here, webpack will take care of it for us,
// including full page reloads if HMR won't work
files: [
'src/*.html'
]
});

+ 83
- 0
webpack.config.js View File

@ -0,0 +1,83 @@
// For info about this file refer to webpack and webpack-hot-middleware documentation
// Rather than having hard coded webpack.config.js for each environment, this
// file generates a webpack config for the environment passed to the getConfig method.
import webpack from 'webpack';
import path from 'path';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
const developmentEnvironment = 'development' ;
const productionEnvironment = 'production';
const testEnvironment = 'test';
const getPlugins = function (env) {
const GLOBALS = {
'process.env.NODE_ENV': JSON.stringify(env),
__DEV__: env === developmentEnvironment
};
const plugins = [
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.DefinePlugin(GLOBALS) //Tells React to build in prod mode. https://facebook.github.io/react/downloads.html
];
switch (env) {
case productionEnvironment:
plugins.push(new ExtractTextPlugin('styles.css'));
plugins.push(new webpack.optimize.DedupePlugin());
plugins.push(new webpack.optimize.UglifyJsPlugin());
break;
case developmentEnvironment:
plugins.push(new webpack.HotModuleReplacementPlugin());
plugins.push(new webpack.NoErrorsPlugin());
break;
}
return plugins;
};
const getEntry = function (env) {
const entry = [];
if (env === developmentEnvironment ) { // only want hot reloading when in dev.
entry.push('webpack-hot-middleware/client');
}
entry.push('./src/index');
return entry;
};
const getLoaders = function (env) {
const loaders = [{ test: /\.js$/, include: path.join(__dirname, 'src'), loaders: ['babel', 'eslint'] }];
if (env === productionEnvironment ) {
// generate separate physical stylesheet for production build using ExtractTextPlugin. This provides separate caching and avoids a flash of unstyled content on load.
loaders.push({test: /(\.css|\.scss)$/, loader: ExtractTextPlugin.extract("css?sourceMap!sass?sourceMap")});
} else {
loaders.push({test: /(\.css|\.scss)$/, loaders: ['style', 'css?sourceMap', 'sass?sourceMap']});
}
return loaders;
};
function getConfig(env) {
return {
debug: true,
devtool: env === productionEnvironment ? 'source-map' : 'cheap-module-eval-source-map', // more info:https://webpack.github.io/docs/build-performance.html#sourcemaps and https://webpack.github.io/docs/configuration.html#devtool
noInfo: true, // set to false to see a list of every file being bundled.
entry: getEntry(env),
target: env === testEnvironment ? 'node' : 'web', // necessary per https://webpack.github.io/docs/testing.html#compile-and-test
output: {
path: __dirname + '/dist', // Note: Physical files are only output by the production build task `npm run build`.
publicPath: '',
filename: 'bundle.js'
},
plugins: getPlugins(env),
module: {
loaders: getLoaders(env)
}
};
}
export default getConfig;

Loading…
Cancel
Save