-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
640 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<!doctype html> | ||
<html> | ||
<body> | ||
<a href="#">Home</a> - <a href="#/results">Results</a> | ||
<div id="app"></div> | ||
<script src="bundle.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
{ | ||
"name": "voting-client", | ||
"version": "1.0.0", | ||
"description": "A client for handling a list of entries to determine the group's favorite. This project is fully based on http://teropa.info/blog/2015/09/10/full-stack-redux-tutorial.html", | ||
"main": "index.js", | ||
"babel": { | ||
"presets": [ | ||
"es2015", | ||
"react" | ||
] | ||
}, | ||
"scripts": { | ||
"test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js \"test/**/*@(.js|.jsx)\"", | ||
"test:watch": "npm run test -- --watch" | ||
}, | ||
"author": "Remy Parzinski", | ||
"license": "ISC", | ||
"devDependencies": { | ||
"babel-core": "^6.10.4", | ||
"babel-loader": "^6.2.4", | ||
"babel-preset-es2015": "^6.9.0", | ||
"babel-preset-react": "^6.11.1", | ||
"chai": "^3.5.0", | ||
"chai-immutable": "^1.6.0", | ||
"jsdom": "^9.4.1", | ||
"mocha": "^2.5.3", | ||
"react-hot-loader": "^1.3.0", | ||
"webpack": "^1.13.1", | ||
"webpack-dev-server": "^1.14.1" | ||
}, | ||
"dependencies": { | ||
"immutable": "^3.8.1", | ||
"react": "^15.2.0", | ||
"react-addons-pure-render-mixin": "^15.2.0", | ||
"react-addons-test-utils": "^15.2.0", | ||
"react-dom": "^15.2.0", | ||
"react-redux": "^4.4.5", | ||
"react-router": "^2.0.0", | ||
"redux": "^3.5.2", | ||
"socket.io-client": "^1.4.8" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
export function setState(state) { | ||
return { | ||
type: 'SET_STATE', | ||
state | ||
}; | ||
} | ||
|
||
export function vote(entry) { | ||
return { | ||
type: 'VOTE', | ||
entry, | ||
meta: { | ||
remote: true | ||
} | ||
}; | ||
} | ||
|
||
export function next() { | ||
return { | ||
type: 'NEXT', | ||
meta: { | ||
remote: true | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import React from 'react'; | ||
|
||
export default React.createClass({ | ||
|
||
render: function() { | ||
return this.props.children; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import React from 'react'; | ||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | ||
import {connect} from 'react-redux'; | ||
import Winner from './Winner'; | ||
import * as actionCreators from '../action_creators'; | ||
|
||
export const Results = React.createClass({ | ||
mixins: [PureRenderMixin], | ||
|
||
getPair: function() { | ||
return this.props.pair || []; | ||
}, | ||
|
||
getVotes: function(entry) { | ||
if (this.props.tally && this.props.tally.has(entry)) { | ||
return this.props.tally.get(entry); | ||
} | ||
return 0; | ||
}, | ||
|
||
render: function() { | ||
return this.props.winner ? | ||
<Winner ref="winner" winner={this.props.winner} /> : | ||
<div className="results"> | ||
<div className="tally"> | ||
{this.getPair().map(entry => | ||
<div key={entry} className="entry"> | ||
<h1>{entry}</h1> | ||
<div className="voteCount"> | ||
{this.getVotes(entry)} | ||
</div> | ||
</div> | ||
)} | ||
</div> | ||
<div className="management"> | ||
<button ref="next" className="next" onClick={this.props.next}> | ||
Next | ||
</button> | ||
</div> | ||
</div>; | ||
} | ||
}); | ||
|
||
function mapStateToProps(state) { | ||
return { | ||
pair: state.getIn(['vote', 'pair']), | ||
tally: state.getIn(['vote', 'tally']), | ||
winner: state.get('winner') | ||
} | ||
} | ||
|
||
export const ResultsContainer = connect( | ||
mapStateToProps, | ||
actionCreators | ||
)(Results); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import React from 'react'; | ||
import {PureRenderMixin} from 'react-addons-pure-render-mixin'; | ||
|
||
export default React.createClass({ | ||
mixins: [PureRenderMixin], | ||
|
||
getPair: function() { | ||
return this.props.pair || []; | ||
}, | ||
|
||
isDisabled: function() { | ||
return !! this.props.hasVoted; | ||
}, | ||
|
||
hasVotedFor: function(entry) { | ||
return this.props.hasVoted === entry; | ||
}, | ||
|
||
render: function() { | ||
return <div className="voting"> | ||
{this.getPair().map(entry => | ||
<button key={entry} | ||
disabled={this.isDisabled()} | ||
onClick={() => this.props.vote(entry) }> | ||
<h1 className="btn-title">{entry}</h1> | ||
{this.hasVotedFor(entry) ? | ||
<div className="label">Voted</div> : | ||
null} | ||
</button> | ||
)} | ||
</div>; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from 'react'; | ||
import {PureRenderMixin} from 'react-addons-pure-render-mixin'; | ||
import {connect} from 'react-redux'; | ||
import Winner from './Winner'; | ||
import Vote from './Vote'; | ||
import * as actionCreators from '../action_creators'; | ||
|
||
export const Voting = React.createClass({ | ||
mixins: [PureRenderMixin], | ||
|
||
render: function() { | ||
return <div> | ||
{this.props.winner ? | ||
<Winner ref="winner" winner={this.props.winner} /> : | ||
<Vote {...this.props} />} | ||
</div>; | ||
} | ||
}); | ||
|
||
function mapStateToProps(state) { | ||
return { | ||
pair: state.getIn(['vote', 'pair']), | ||
hasVoted: state.get('hasVoted'), | ||
winner: state.get('winner') | ||
}; | ||
} | ||
|
||
export const VotingContainer = connect( | ||
mapStateToProps, | ||
actionCreators | ||
)(Voting); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import React from 'react'; | ||
import {PureRenderMixin} from 'react-addons-pure-render-mixin'; | ||
|
||
export default React.createClass({ | ||
mixins: [PureRenderMixin], | ||
|
||
render: function() { | ||
return <div className="winner"> | ||
Winner is {this.props.winner}! | ||
</div>; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import {Router, Route, hashHistory} from 'react-router'; | ||
|
||
import {createStore, applyMiddleware} from 'redux'; | ||
import {Provider} from 'react-redux'; | ||
import io from 'socket.io-client'; | ||
import reducer from './reducer'; | ||
|
||
import {setState} from './action_creators'; | ||
import remoteActionMiddleware from './remote_action_middleware'; | ||
|
||
import App from './components/App'; | ||
import {VotingContainer} from './components/Voting'; | ||
import {ResultsContainer} from './components/Results'; | ||
|
||
const socket = io(`${location.protocol}//${location.hostname}:8090`); | ||
socket.on('state', function(state) { | ||
store.dispatch(setState(state)); | ||
}); | ||
|
||
const createStoreWithMiddleware = applyMiddleware( | ||
remoteActionMiddleware(socket) | ||
)(createStore); | ||
const store = createStoreWithMiddleware(reducer); | ||
|
||
const routes = <Route component={App}> | ||
<Route path="/results" component={ResultsContainer} /> | ||
<Route path="/" component={VotingContainer} /> | ||
</Route>; | ||
|
||
ReactDOM.render( | ||
<Provider store={store}> | ||
<Router history={hashHistory}>{routes}</Router> | ||
</Provider>, | ||
document.getElementById('app') | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import {List, Map} from 'immutable'; | ||
|
||
function setState(state, newState) { | ||
return state.merge(newState); | ||
} | ||
|
||
function vote(state, entry) { | ||
const pair = state.getIn(['vote', 'pair']); | ||
|
||
if (pair && pair.includes(entry)) { | ||
return state.set('hasVoted', entry); | ||
} | ||
return state; | ||
} | ||
|
||
function resetVote(state) { | ||
const hasVoted = state.get('hasVoted'); | ||
const pair = state.getIn(['vote', 'pair'], List()); | ||
|
||
if (hasVoted && !pair.includes(hasVoted)) { | ||
return state.remove('hasVoted'); | ||
} | ||
return state; | ||
} | ||
|
||
export default function(state = Map(), action) { | ||
switch(action.type) { | ||
case 'SET_STATE': | ||
return resetVote(setState(state, action.state)); | ||
case 'VOTE': | ||
return vote(state, action.entry); | ||
} | ||
|
||
return state; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export default function(socket) { | ||
return function(store) { | ||
return function(next) { | ||
return function(action) { | ||
if (action.meta && action.meta.remote) { | ||
socket.emit('action', action); | ||
} | ||
|
||
return next(action); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import { | ||
renderIntoDocument, | ||
scryRenderedDOMComponentsWithClass, | ||
Simulate | ||
} from 'react-addons-test-utils'; | ||
import {List, Map} from 'immutable'; | ||
import {Results} from '../../src/components/Results'; | ||
import {expect} from 'chai'; | ||
|
||
describe('Results', function() { | ||
|
||
it('renders entries with vote counts or zero', function() { | ||
const pair = List.of('Trainspotting', '28 Days Later'); | ||
const tally = Map({'Trainspotting': 5}); | ||
const component = renderIntoDocument( | ||
<Results pair={pair} tally={tally} /> | ||
); | ||
const entries = scryRenderedDOMComponentsWithClass(component, 'entry'); | ||
const [train, days] = entries.map(function(entry) { | ||
return entry.textContent; | ||
}); | ||
|
||
|
||
expect(entries.length).to.equal(2); | ||
expect(train).to.contain('Trainspotting'); | ||
expect(train).to.contain('5'); | ||
expect(days).to.contain('28 Days Later'); | ||
expect(days).to.contain('0'); | ||
}); | ||
|
||
it('invokes the next callback when the next button is clicked', function() { | ||
let nextInvoked = false; | ||
const next = function() { | ||
nextInvoked = true; | ||
} | ||
const pair = List.of('Trainspotting', '28 Days Later'); | ||
const component = renderIntoDocument( | ||
<Results pair={pair} | ||
tally={Map()} | ||
next={next} /> | ||
); | ||
Simulate.click(ReactDOM.findDOMNode(component.refs.next)); | ||
|
||
expect(nextInvoked).to.equal(true); | ||
}); | ||
|
||
it('renders the winner when there is one', function() { | ||
const component = renderIntoDocument( | ||
<Results winner="Trainspotting" | ||
pair={['Trainspotting', '28 Days Later']} | ||
tally={Map()} /> | ||
); | ||
const winner = ReactDOM.findDOMNode(component.refs.winner); | ||
|
||
expect(winner).to.be.ok; | ||
expect(winner.textContent).to.contain('Trainspotting'); | ||
}); | ||
}); |
Oops, something went wrong.