Skip to content

Commit

Permalink
Merge pull request #63 from hunterjm/feature/playerSettings
Browse files Browse the repository at this point in the history
Individual Player Settings
  • Loading branch information
hunterjm authored Jan 28, 2017
2 parents 4fac057 + d2a1ec9 commit c866ae1
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 12 deletions.
2 changes: 2 additions & 0 deletions app/actions/bid.js
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@ export function updatePrice(player, settings) {
dispatch(addMessage('log', `Updating price for ${player.name}...`));
await dispatch(findPrice(player.id));
}
} else {
dispatch(addMessage('warn', `Auto update prices is disabled for ${player.name}...`));
}
};
}
Expand Down
4 changes: 4 additions & 0 deletions app/actions/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ export function setPrice(id, price) {
return { type: types.SET_PRICE, id, price };
}

export function setSetting(id, key, value) {
return { type: types.SET_SETTING, id, key, value };
}

export function add(player) {
metrics.track('Add Player', {
id: player.id,
Expand Down
1 change: 1 addition & 0 deletions app/actions/playerTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const ADD_PLAYER = 'ADD_PLAYER';
export const REMOVE_PLAYER = 'REMOVE_PLAYER';
export const CLEAR_LIST = 'CLEAR_LIST';
export const SET_PRICE = 'SET_PRICE';
export const SET_SETTING = 'player/set/setting';
14 changes: 11 additions & 3 deletions app/components/player/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ class Header extends Component {
const player = this.props.player;
const tabBioClasses = classNames({
'details-tab': true,
active: !this.props.router.isActive(`/players/${player.id}/history`),
active: (
!this.props.router.isActive(`/players/${player.id}/history`)
&& !this.props.router.isActive(`/players/${player.id}/settings`)
),
});
const tabSettingsClasses = classNames({
const tabHistoryClasses = classNames({
'details-tab': true,
active: this.props.router.isActive(`/players/${player.id}/history`),
});
const tabSettingsClasses = classNames({
'details-tab': true,
active: this.props.router.isActive(`/players/${player.id}/settings`),
});
return (
<div>
<div className="header-section">
Expand Down Expand Up @@ -51,7 +58,8 @@ class Header extends Component {
</div>
<div className="details-subheader-tabs">
<span className={tabBioClasses}><Link to={`/players/${player.id}`}>Bio</Link></span>
<span className={tabSettingsClasses}><Link to={`/players/${player.id}/history`}>History</Link></span>
<span className={tabHistoryClasses}><Link to={`/players/${player.id}/history`}>History</Link></span>
<span className={tabSettingsClasses}><Link to={`/players/${player.id}/settings`}>Settings</Link></span>
</div>
</div>
</div>
Expand Down
244 changes: 244 additions & 0 deletions app/components/player/PlayerSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Fut from 'fut-promise';
import Header from './Header';
import * as PlayerActions from '../../actions/player';

export class PlayerSettings extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
errors: {}
};
this.player = props.player.list[props.params.id];
}

componentDidMount() {
this.maxCardInput.focus();
}

componentWillReceiveProps(nextProps) {
this.setState({ errors: nextProps.errors || {} });
}

shouldComponentUpdate(nextProps, nextState) {
return (nextState.loading !== this.state.loading
|| (nextState.errors && nextState.errors !== this.state.errors));
}

componentWillUpdate(nextProps) {
this.player = nextProps.player.list[nextProps.params.id];
}

validate() {
const errors = {};
const price = _.get(this.player, 'price', {});

if (!Fut.isPriceValid(price.buy)) {
errors.buy = 'Must be a valid price';
}

if (!Fut.isPriceValid(price.sell)) {
errors.sell = 'Must be a valid price';
}

if (!Fut.isPriceValid(price.bin)) {
errors.bin = 'Must be a valid price';
}

return errors;
}

handleChange(event) {
let value = event.target.value;
if (event.target.type === 'checkbox') {
value = event.target.checked;
}
this.props.setSetting(this.player.id, event.target.name, value);
}

handlePriceChange(event) {
const price = _.merge({}, this.player.price);
price[event.target.name] = event.target.value;
this.props.setPrice(this.player.id, price);
}

handlePriceBlur(event) {
this.setState({ errors: _.omitBy(
this.validate(),
(val, key) => (
key !== event.target.name
&& (
!_.get(this.player, `settings[${key}].length`, 0)
|| !_.get(this.player, `price[${key}].length`, 0)
)
)
) });
}

render() {
const { maxCard, snipeOnly, autoUpdate, relistAll } = _.get(this.player, 'settings', {});
const { buy, sell, bin } = _.get(this.player, 'price', {});
const defaultSettings = _.get(this.props, 'settings', {});
return (
<div className="details">
<Header
player={this.player}
router={this.context.router}
/>
<div className="details-panel">
<div className="settings">
<div className="settings-panel preferences">
<div className="settings-section preferences-content">
<div className="title" style={{ marginTop: 0 }}>Player Settings</div>
<div className="global">
<div className="option">
<div className="option-name">
<label htmlFor="maxCard">Max Cards</label>
<p><small>Maximum number of this player allowed in transfer list</small></p>
</div>
<div className="option-value">
<input
ref={maxCardInput => (this.maxCardInput = maxCardInput)} maxLength="3" name="maxCard"
placeholder={defaultSettings.maxCard} value={maxCard || ''} type="text"
onChange={this.handleChange.bind(this)}
/>
<p className="error-message">{this.state.errors.maxCard}</p>
</div>
</div>
<div className="option">
<div className="option-name">
<label htmlFor="snipeOnly">BIN Snipe Only</label>
<p><small>
Only purchase this player for buy it now price, no bidding
</small></p>
</div>
<div className="option-value">
<input
ref={snipeOnlyInput => (this.snipeOnlyInput = snipeOnlyInput)} name="snipeOnly"
checked={snipeOnly !== undefined ? snipeOnly : defaultSettings.snipeOnly}
type="checkbox" onChange={this.handleChange.bind(this)}
/>
</div>
</div>
</div>
<div className="title">Price Settings</div>
<div className="price">
<div className="option">
<div className="option-name">
<label htmlFor="autoUpdate">Automatically Update Prices</label>
<p><small>Updates every hour based on lowest listed BIN price</small></p>
</div>
<div className="option-value">
<input
ref={autoUpdateInput => (this.autoUpdateInput = autoUpdateInput)} name="autoUpdate"
checked={autoUpdate !== undefined ? autoUpdate : defaultSettings.autoUpdate}
type="checkbox" onChange={this.handleChange.bind(this)}
/>
</div>
</div>
<div className="option">
<div className="option-name">
<label htmlFor="buy">Purchase Price</label>
<p><small>
Price you want to buy the player at
</small></p>
</div>
<div className="option-value">
<input
ref={buyInput => (this.buyInput = buyInput)} maxLength="9" name="buy" placeholder="Buy"
value={buy} type="text" onChange={this.handlePriceChange.bind(this)} onBlur={this.handlePriceBlur.bind(this)}
/>
<p className="error-message">{this.state.errors.buy}</p>
</div>
</div>
<div className="option">
<div className="option-name">
<label htmlFor="sell">List Price</label>
<p><small>
Price you want to list the player at
</small></p>
</div>
<div className="option-value">
<input
ref={sellInput => (this.sellInput = sellInput)} maxLength="3" name="sell" placeholder="Sell"
value={sell} type="text" onChange={this.handlePriceChange.bind(this)} onBlur={this.handlePriceBlur.bind(this)}
/>
<p className="error-message">{this.state.errors.sell}</p>
</div>
</div>
<div className="option">
<div className="option-name">
<label htmlFor="bin">Listed BIN Price</label>
<p><small>
Price you want to set listed BIN at
</small></p>
</div>
<div className="option-value">
<input
ref={binInput => (this.binInput = binInput)} maxLength="3" name="bin" placeholder="BIN"
value={bin} type="text" onChange={this.handlePriceChange.bind(this)} onBlur={this.handlePriceBlur.bind(this)}
/>
<p className="error-message">{this.state.errors.bin}</p>
</div>
</div>
<div className="option">
<div className="option-name">
<label htmlFor="relistAll">Same Relist Price</label>
<p><small>
Relist players at the prices they were bought for if market price changes
<br />
(risks tying up capital that could otherwise be used
to make up the difference)
</small></p>
</div>
<div className="option-value">
<input
ref={relistAllInput => (this.relistAllInput = relistAllInput)} name="relistAll"
checked={relistAll !== undefined ? relistAll : defaultSettings.relistAll}
type="checkbox" onChange={this.handleChange.bind(this)}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}

PlayerSettings.propTypes = {
setSetting: PropTypes.func.isRequired,
setPrice: PropTypes.func.isRequired,
params: PropTypes.shape({
id: PropTypes.int
}),
player: PropTypes.shape({
list: PropTypes.shape({})
}),
settings: PropTypes.shape({})
};

PlayerSettings.contextTypes = {
router: PropTypes.object.isRequired,
store: PropTypes.object
};

function mapStateToProps(state) {
return {
player: state.player,
settings: state.settings
};
}

function mapDispatchToProps(dispatch) {
return bindActionCreators(PlayerActions, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(PlayerSettings);
10 changes: 10 additions & 0 deletions app/reducers/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function player(state = initialState, action) {
_.set(nextState, `list.${_.get(action, 'player.id')}`, action.player);
// Setup additional information
_.set(nextState, `list.${_.get(action, 'player.id')}.price`, {});
_.set(nextState, `list.${_.get(action, 'player.id')}.settings`, {});
return nextState;
}
case types.REMOVE_PLAYER: {
Expand All @@ -35,6 +36,15 @@ export function player(state = initialState, action) {
_.set(nextState, `list.${action.id}.price`, action.price);
return nextState;
}
case types.SET_SETTING: {
const nextState = _.merge({}, state);
if (action.value !== '') {
_.set(nextState, `list.${action.id}.settings.${action.key}`, action.value);
} else {
_.unset(nextState, `list.${action.id}.settings.${action.key}`);
}
return nextState;
}
default:
return state;
}
Expand Down
2 changes: 2 additions & 0 deletions app/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ConnectedPlayers from './containers/Players';
import ConnectedPlayerSearch from './components/player/PlayerSearch';
import ConnectedPlayerDetails from './components/player/PlayerDetails';
import ConnectedPlayerHistory from './components/player/PlayerHistory';
import ConnectedPlayerSettings from './components/player/PlayerSettings';
import ConnectedBidOverview from './components/bid/Overview';
import ConnectedBidLogs from './components/bid/Logs';
import ConnectedSettings from './components/Settings';
Expand All @@ -20,6 +21,7 @@ export default (
<Route path=":id">
<IndexRoute component={ConnectedPlayerDetails} />
<Route path="history" component={ConnectedPlayerHistory} />
<Route path="settings" component={ConnectedPlayerSettings} />
</Route>
</Route>
<Route path="settings" component={ConnectedPlayers}>
Expand Down
26 changes: 17 additions & 9 deletions test/actions/player.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import sinon from 'sinon';
import nock from 'nock';
import request from 'request';
import { expect } from 'chai';
import * as ApiUtil from '../../app/utils/ApiUtil';
import * as actions from '../../app/actions/player';
Expand All @@ -24,6 +24,15 @@ describe('actions', () => {
);
});

it('setSetting should create SET_SETTING action', () => {
const id = '123456';
const key = 'buy';
const value = '1000';
expect(actions.setSetting(id, key, value)).to.eql(
{ type: types.SET_SETTING, id, key, value }
);
});

it('add should create ADD_PLAYER action', () => {
const player = { id: '123456' };
expect(actions.add(player)).to.eql(
Expand Down Expand Up @@ -54,7 +63,7 @@ describe('actions', () => {
clock.restore();
});

it('should dispatch SAVE_SEARCH_RESULTS when search() is completed', () => {
it('should dispatch SAVE_SEARCH_RESULTS when search() is completed', async () => {
// Mock search response
const results = {
count: 3,
Expand All @@ -68,16 +77,15 @@ describe('actions', () => {
totalResults: 3,
type: 'FUTPlayerItemList'
};
nock('https://www.easports.com')
.get('/uk/fifa/ultimate-team/api/fut/item').query(true)
.reply(200, results);
const requestStub = sandbox.stub(request, 'get')
.yields(null, { statusCode: 200 }, JSON.stringify(results))
.returns({ abort: sandbox.spy() });

const store = mockStore({});

return store.dispatch(actions.search('messi'))
.then(() => { // return of async actions
expect(store.getActions()).to.include({ type: types.SAVE_SEARCH_RESULTS, results });
});
await store.dispatch(actions.search('messi', 1));
expect(requestStub.calledOnce).to.eql(true);
expect(store.getActions()).to.include({ type: types.SAVE_SEARCH_RESULTS, results });
});

it('should dispatch SET_PRICE when findPrice() is completed', async () => {
Expand Down
Loading

0 comments on commit c866ae1

Please sign in to comment.