Browse Source

Remove all example code, add websocket.

pull/1/head
Titouan Rigoudy 9 years ago
parent
commit
2063a93d68
28 changed files with 237 additions and 762 deletions
  1. +0
    -9
      src/actions/fuelSavingsActions.js
  2. +38
    -0
      src/actions/socketActionsFactory.js
  3. +34
    -0
      src/actions/socketHandlerActions.js
  4. +0
    -10
      src/businessLogic/dateHelper.js
  5. +0
    -26
      src/businessLogic/dateHelper.spec.js
  6. +0
    -67
      src/businessLogic/fuelSavingsCalculator.js
  7. +0
    -123
      src/businessLogic/fuelSavingsCalculator.spec.js
  8. +0
    -40
      src/businessLogic/mathHelper.js
  9. +0
    -60
      src/businessLogic/mathHelper.spec.js
  10. +0
    -58
      src/businessLogic/numberFormatter.js
  11. +0
    -38
      src/businessLogic/numberFormatter.spec.js
  12. +0
    -72
      src/components/FuelSavingsApp.js
  13. +0
    -8
      src/components/FuelSavingsApp.spec.js
  14. +0
    -49
      src/components/FuelSavingsResults.js
  15. +0
    -63
      src/components/FuelSavingsResults.spec.js
  16. +0
    -31
      src/components/FuelSavingsTextInput.js
  17. +21
    -0
      src/components/SolsticeApp.js
  18. +7
    -2
      src/constants/ActionTypes.js
  19. +30
    -20
      src/containers/App.js
  20. +1
    -1
      src/index.html
  21. +1
    -0
      src/index.js
  22. +0
    -53
      src/reducers/fuelSavings.js
  23. +2
    -2
      src/reducers/index.js
  24. +53
    -0
      src/reducers/socket.js
  25. +11
    -0
      src/reducers/solstice.js
  26. +2
    -2
      src/store/configureStore.js
  27. +6
    -28
      src/styles/styles.scss
  28. +31
    -0
      src/utils/SocketClient.js

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

@ -1,9 +0,0 @@
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 };
}

+ 38
- 0
src/actions/socketActionsFactory.js View File

@ -0,0 +1,38 @@
import {
SOCKET_SET_OPENING, SOCKET_SET_CLOSING, SOCKET_SEND_MESSAGE
} from "../constants/ActionTypes";
export default (socketClient) => ({
open: url => {
const action = { type: SOCKET_SET_OPENING };
try {
socketClient.open(url);
} catch (err) {
action.error = true;
action.payload = err;
}
return action;
},
close: () => {
const action = { type: SOCKET_SET_CLOSING };
try {
socketClient.close();
} catch (err) {
action.error = true;
action.payload = err;
}
return action;
},
send: message => {
const action = { type: SOCKET_SEND_MESSAGE };
try {
socketClient.send(JSON.stringify(message));
} catch (err) {
action.error = true;
action.payload = err;
}
return action;
}
});

+ 34
- 0
src/actions/socketHandlerActions.js View File

@ -0,0 +1,34 @@
import {
SOCKET_SET_CLOSED,
SOCKET_SET_ERROR,
SOCKET_SET_OPEN,
SOCKET_RECEIVE_MESSAGE
} from "../constants/ActionTypes";
export default {
onclose: event => ({
type: SOCKET_SET_CLOSED,
payload: {
code: event.code
}
}),
onerror: event => ({
type: SOCKET_SET_ERROR
}),
onopen: event => ({
type: SOCKET_SET_OPEN
}),
onmessage: event => {
const action = { type: SOCKET_RECEIVE_MESSAGE };
try {
action.payload = JSON.parse(event.data);
} catch (err) {
action.error = true;
action.payload = err;
}
return action;
}
};

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

@ -1,10 +0,0 @@
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;
}
}

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

@ -1,26 +0,0 @@
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');
});
});
});

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

@ -1,67 +0,0 @@
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;

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

@ -1,123 +0,0 @@
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);
});
});
});

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

@ -1,40 +0,0 @@
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;

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

@ -1,60 +0,0 @@
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);
});
});
});

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

@ -1,58 +0,0 @@
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;

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

@ -1,38 +0,0 @@
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);
});
});
});

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

@ -1,72 +0,0 @@
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;

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

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

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

@ -1,49 +0,0 @@
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;

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

@ -1,63 +0,0 @@
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');
});
});
});

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

@ -1,31 +0,0 @@
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;

+ 21
- 0
src/components/SolsticeApp.js View File

@ -0,0 +1,21 @@
import React, {PropTypes} from 'react';
const App = (props) => {
const { onClick, socket } = props;
return (
<div>
<h1>Solstice web UI</h1>
<div>Socket state: {socket.state}</div>
<button onClick={onClick}>
Connect
</button>
</div>
);
};
App.propTypes = {
onClick: PropTypes.func.isRequired,
socket: PropTypes.object.isRequired
};
export default App;

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

@ -1,2 +1,7 @@
export const SAVE_FUEL_SAVINGS = 'SAVE_FUEL_SAVINGS';
export const CALCULATE_FUEL_SAVINGS = 'CALCULATE_FUEL_SAVINGS';
export const SOCKET_SET_OPEN = Symbol("SOCKET_SET_OPEN");
export const SOCKET_SET_OPENING = Symbol("SOCKET_SET_OPENING");
export const SOCKET_SET_CLOSED = Symbol("SOCKET_SET_CLOSED");
export const SOCKET_SET_CLOSING = Symbol("SOCKET_SET_CLOSING");
export const SOCKET_SET_ERROR = Symbol("SOCKET_SET_ERROR");
export const SOCKET_RECEIVE_MESSAGE = Symbol("SOCKET_RECEIVE_MESSAGE");
export const SOCKET_SEND_MESSAGE = Symbol("SOCKET_SEND_MESSAGE");

+ 30
- 20
src/containers/App.js View File

@ -1,37 +1,47 @@
// This file bootstraps the app with the boilerplate necessary // This file bootstraps the app with the boilerplate necessary
// to support hot reloading in Redux // 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';
import React, {PropTypes} from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
class App extends React.Component {
render() {
import SolsticeApp from "../components/SolsticeApp";
import socketActionsFactory from "../actions/socketActionsFactory";
import socketHandlerActions from "../actions/socketHandlerActions";
import SocketClient from "../utils/SocketClient";
const App = (props) => {
const onClick = (event) => {
const url = "ws://localhost:2244";
props.actions.socketActions.open(url);
};
return ( return (
<FuelSavingsApp appState={this.props.appState} actions={this.props.actions}/>
<SolsticeApp socket={props.socket} onClick={onClick} />
); );
}
}
};
App.propTypes = { App.propTypes = {
actions: PropTypes.object.isRequired,
appState: PropTypes.object.isRequired
actions: PropTypes.object.isRequired,
socket: PropTypes.object.isRequired
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return {
appState: state.fuelSavingsAppState
};
return {
socket: state.socket
};
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
};
const callbacks = bindActionCreators(socketHandlerActions, dispatch);
const socketClient = new SocketClient(callbacks);
const socketActions = socketActionsFactory(socketClient);
return {
actions: {
socketActions: bindActionCreators(socketActions, dispatch)
}
};
} }
export default connect( export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(App); )(App);

+ 1
- 1
src/index.html View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>React Slingshot</title>
<title>Solstice Web UI</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>


+ 1
- 0
src/index.js View File

@ -4,6 +4,7 @@ import { Provider } from 'react-redux';
import App from './containers/App'; import App from './containers/App';
import configureStore from './store/configureStore'; 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. 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.
import SocketClient from "./utils/SocketClient";
const store = configureStore(); const store = configureStore();


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

@ -1,53 +0,0 @@
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;
}
}

+ 2
- 2
src/reducers/index.js View File

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

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

@ -0,0 +1,53 @@
import objectAssign from "object-assign";
import * as types from "../constants/ActionTypes";
const STATE_OPENING = 0;
const STATE_OPEN = 1;
const STATE_CLOSING = 2;
const STATE_CLOSED = 3;
const initialState = {
state: STATE_CLOSED,
url: null
};
export default function socket(state = initialState, action) {
switch (action.type) {
case types.SOCKET_SET_OPENING:
if (action.error) {
return state;
}
return objectAssign({}, state, { state: STATE_OPENING });
case types.SOCKET_SET_OPEN:
return objectAssign({}, state, { state: STATE_OPEN });
case types.SOCKET_SET_CLOSING:
if (action.error) {
return state;
}
return objectAssign({}, state, { state: STATE_CLOSING });
case types.SOCKET_SET_CLOSED:
return objectAssign({}, state, { state: STATE_CLOSED });
case types.SOCKET_SET_ERROR:
if (state.state === STATE_OPENING) {
return objectAssign({}, state, { state: STATE_CLOSED });
}
return state;
case types.SOCKET_RECEIVE_MESSAGE:
console.log(`Socket received message: ${action.payload}`);
return state;
case types.SOCKET_SEND_MESSAGE:
console.log("Sending message");
return state;
default:
return state;
}
}

+ 11
- 0
src/reducers/solstice.js View File

@ -0,0 +1,11 @@
import objectAssign from 'object-assign';
const initialState = {
socketConnected: false,
clientConnected: false,
loggedIn: false
};
export default function solsticeAppState(state = initialState, action) {
return state;
}

+ 2
- 2
src/store/configureStore.js View File

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

+ 6
- 28
src/styles/styles.scss View File

@ -1,31 +1,9 @@
/* Variables */
$vin-blue: #5bb7db;
$vin-green: #60b044;
$vin-red: #ff0000;
/* Styles */ /* Styles */
body { 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;
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
-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; }

+ 31
- 0
src/utils/SocketClient.js View File

@ -0,0 +1,31 @@
import { bindActionCreators } from "redux";
import objectAssign from "object-assign";
const STATE_CONNECTING = 0;
const STATE_OPEN = 1;
const STATE_CLOSING = 2;
const STATE_CLOSED = 3;
class SocketClient {
constructor(callbacks) {
this.callbacks = callbacks;
}
open(url) {
if (this.socket && this.socket.readyState !== STATE_CLOSED) {
throw new Error("SocketClient: socket already open");
}
this.socket = new WebSocket(url);
objectAssign(this.socket, this.callbacks);
}
close() {
this.socket.close();
}
send(message) {
this.socket.send(message);
}
}
export default SocketClient;

Loading…
Cancel
Save