diff --git a/package-lock.json b/package-lock.json index de01ed43a..757faaef7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "bookie", - "version": "1.3.0-rc.0", + "version": "1.3.0-sportsbook.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 06f044196..3f3c157f5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bookie", "author": "Peerplays Blockchain Standards Association", "description": "Electron app powered by CRA", - "version": "1.3.0-rc.0", + "version": "1.3.0-sportsbook.1", "private": true, "homepage": "./", "devDependencies": { diff --git a/src/actions/AppActions.js b/src/actions/AppActions.js index ace2a3b23..3dcf58ab2 100644 --- a/src/actions/AppActions.js +++ b/src/actions/AppActions.js @@ -89,6 +89,16 @@ class AppActions { }; } + /** + * Action to set the Book mode ( Exchange or SportsBook ) + */ + static setBookMode(mode) { + return { + type: ActionTypes.APP_SET_BOOK_MODE, + mode + }; + } + /** * Action to show logout popup */ diff --git a/src/actions/BetActions.js b/src/actions/BetActions.js index 67b132d19..e7981219e 100644 --- a/src/actions/BetActions.js +++ b/src/actions/BetActions.js @@ -527,7 +527,7 @@ class BetActions { betDiff.map((item) => { item[0] = CurrencyUtils.correctFloatingPointPrecision([item[0], item[2]], currencyType); - if(item[1] === 'decrement') { + if (item[1] === 'decrement') { changeType = BetTypes.DECREMENT; } diff --git a/src/actions/MarketDrawerActions.js b/src/actions/MarketDrawerActions.js index 7d8852469..6ceb04656 100644 --- a/src/actions/MarketDrawerActions.js +++ b/src/actions/MarketDrawerActions.js @@ -265,6 +265,178 @@ class MarketDrawerActions { }; } + static getOpenBetsForEvent(eventId) { + return (dispatch, getState) => { + const bmgs = getState().getIn(['bettingMarketGroup', 'bettingMarketGroupsById']); + + if (!bmgs || bmgs.isEmpty()) { + return null; + } + + let openUnmatchedBets = Immutable.List(); + let openMatchedBets = Immutable.List(); + + // For each BMG that belongs to the event + bmgs.forEach((bmg) => { + if (bmg.get('event_id') === eventId) { + // Get the associated bets and push them into their respective list. Forming a single list + let bets = MarketDrawerActions.getOpenBetsForBMG(getState(), bmg.get('id')); + bets.openUnmatchedBets.forEach((bet) => { + openUnmatchedBets = openUnmatchedBets.push(bet); + }); + + bets.openMatchedBets.forEach((bet) => { + openMatchedBets = openMatchedBets.push(bet); + }); + } + }); + + // Send the list off to the private action, to be added to state. + // - Pass in the eventId so that Bookie knows that we're on a event page. + // - Pass in null for the bettingMarketGroupId so that Bookie knows we're not on a BMG page. + dispatch( + MarketDrawerPrivateActions.getOpenBets( + openUnmatchedBets, + openMatchedBets, + null, // betting Market Group Id + eventId + ) + ); + }; + } + + static getOpenBetsForBMG(state, bettingMarketGroupId) { + + const bettingMarketGroup = state.getIn([ + 'bettingMarketGroup', + 'bettingMarketGroupsById', + bettingMarketGroupId + ]); + + if (!bettingMarketGroup || bettingMarketGroup.isEmpty()) { + // If betting market group doesn't exist, clear open bets + return null; + } else { + const unmatchedBetsById = state.getIn(['bet', 'unmatchedBetsById']); + const matchedBetsById = state.getIn(['bet', 'matchedBetsById']); + + const bettingMarketsById = state.getIn(['bettingMarket', 'bettingMarketsById']); + const assetsById = state.getIn(['asset', 'assetsById']); + const bettingMarketGroupDescription = + bettingMarketGroup && bettingMarketGroup.get('description'); + + // Helper function to filter related bet + const filterRelatedBet = (bet) => { + // Only get bet that belongs to this betting market group + const bettingMarket = bettingMarketsById.get(bet.get('betting_market_id')); + return bettingMarket && bettingMarket.get('group_id') === bettingMarketGroupId; + }; + + // Helper function to format bets to market drawer bet object structure + const formatBet = (bet) => { + const accountId = state.getIn(['account', 'account', 'id']); + const setting = + state.getIn(['setting', 'settingByAccountId', accountId]) || + state.getIn(['setting', 'defaultSetting']); + const bettingMarket = bettingMarketsById.get(bet.get('betting_market_id')); + const bettingMarketDescription = bettingMarket && bettingMarket.get('description'); + const precision = + assetsById.get(bettingMarketGroup.get('asset_id')).get('precision') || 0; + const odds = bet.get('backer_multiplier'); + const betType = bet.get('back_or_lay'); + const currencyFormat = setting.get('currencyFormat'); + + // Get the stake from the bet object + // BACK: The stake is present in a back bet by default + // LAY: The backer's stake needs to be calculated from the values recorded in the + // lay bet on the blockchain + let stake = ObjectUtils.getStakeFromBetObject(bet) / Math.pow(10, precision); + + // This if statement sets the profit/liability according to the kind of bet it is. + // Values are then converted to string format for consistent comparison + // BACK: ALWAYS have a liability of 0, profit to be calculated + // LAY: ALWAYS have a profit of 0, profit to be calculated + let profit = 0, + profitAsString = '0'; + let liability = 0, + liabilityAsString = '0'; + + if (betType === BetTypes.BACK) { + // Get the raw value of the profit + profit = bet.get('original_profit') / Math.pow(10, precision); + profitAsString = CurrencyUtils.formatFieldByCurrencyAndPrecision( + 'profit', + profit, + currencyFormat + ).toString(); // Record it as a string + } else if (betType === BetTypes.LAY) { + liability = bet.get('original_liability') / Math.pow(10, precision); + liabilityAsString = CurrencyUtils.formatFieldByCurrencyAndPrecision( + 'liability', + liability, + currencyFormat + ).toString(); + } else { + console.error('Serious Error - Bet with no type has been detected'); + } + + // store odds and stake values as String for easier comparison + const oddsAsString = CurrencyUtils.formatFieldByCurrencyAndPrecision( + 'odds', + odds, + currencyFormat + ).toString(); + const stakeAsString = CurrencyUtils.formatFieldByCurrencyAndPrecision( + 'stake', + stake, + currencyFormat + ).toString(); + + let formattedBet = Immutable.fromJS({ + id: bet.get('id'), + original_bet_id: bet.get('original_bet_id'), + bet_type: bet.get('back_or_lay'), + bettor_id: bet.get('bettor_id'), + betting_market_id: bet.get('betting_market_id'), + betting_market_description: bettingMarketDescription, + betting_market_group_description: bettingMarketGroupDescription, + odds: oddsAsString, + stake: stakeAsString, + profit: profitAsString, + liability: liabilityAsString + }); + + if (bet.get('category') === BetCategories.UNMATCHED_BET) { + // Keep all of the original values for precision and accuracy in future calculations + formattedBet = formattedBet + .set('original_odds', oddsAsString) + .set('original_stake', stakeAsString) + .set('original_profit', profitAsString) + .set('original_liability', liabilityAsString) + .set('updated', false); + } + + return formattedBet; + }; + + const openUnmatchedBets = unmatchedBetsById + .filter(filterRelatedBet) + .map(formatBet) + .toList(); + const openMatchedBets = matchedBetsById + .filter(filterRelatedBet) + .map(formatBet) + .toList(); + + return { + openUnmatchedBets, + openMatchedBets + }; + } + } + + + static getOpenBets(bettingMarketGroupId) { return (dispatch, getState) => { const bettingMarketGroup = getState().getIn([ diff --git a/src/components/App/App.less b/src/components/App/App.less index 41b70151f..849adde12 100644 --- a/src/components/App/App.less +++ b/src/components/App/App.less @@ -203,3 +203,27 @@ body { line-height: 1.42857143; //reference from bootstrap color: @primary_label; } + +.sportsBookToggle { + z-index: 100; + margin: 5px; + -webkit-app-region: no-drag; + + p { + display: inline-block; + padding: 2px 6px 2px 6px; + border: 1px solid @cello; + text-transform: uppercase; + + &.active { + background-color: @cello; + color: @update_highlight; + } + + &:hover { + background-color: @cello; + cursor: pointer; + color: @update_highlight; + } + } +} diff --git a/src/components/App/TitleBar/MacTitleBar/MacTitleBar.jsx b/src/components/App/TitleBar/MacTitleBar/MacTitleBar.jsx index e74837796..56aa20588 100644 --- a/src/components/App/TitleBar/MacTitleBar/MacTitleBar.jsx +++ b/src/components/App/TitleBar/MacTitleBar/MacTitleBar.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import Clock from '../Clock'; import {I18n} from 'react-redux-i18n'; import {Config} from '../../../../constants'; +import SportsbookToggle from '../SportsbookToggle'; class MacTitleBar extends PureComponent { render() { @@ -14,6 +15,7 @@ class MacTitleBar extends PureComponent { onResizeClick, onCloseClick, isFullscreen, + loggedIn, ...props } = this.props; @@ -33,6 +35,9 @@ class MacTitleBar extends PureComponent { />
+ {loggedIn && ( + + )}
diff --git a/src/components/App/TitleBar/SportsbookToggle.jsx b/src/components/App/TitleBar/SportsbookToggle.jsx new file mode 100644 index 000000000..eec3ea0e7 --- /dev/null +++ b/src/components/App/TitleBar/SportsbookToggle.jsx @@ -0,0 +1,132 @@ +import React, {PureComponent} from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import PropTypes from 'prop-types'; +import {AppActions, NavigateActions} from '../../../actions'; +import {I18n} from 'react-redux-i18n'; +import {BookieModes} from '../../../constants'; +import {EventPageSelector} from '../../../selectors'; +import {ChainTypes} from 'peerplaysjs-lib'; + +class SportsbookToggle extends PureComponent { + constructor(props) { + super(props); + this.toggle = this.toggle.bind(this); + } + + toggle(mode) { + let subroute = ''; + + if (this.props.sportID) { + subroute = '/sport/' + this.props.sportID; + } else if (this.props.eventGroupID) { + subroute = '/eventgroup/' + this.props.eventGroupID; + } else if (this.props.eventID) { + subroute = '/events/' + this.props.eventID; + } else if (this.props.bmgID) { + subroute = '/BettingMarketGroup/' + this.props.bmgID; + } + + switch (mode) { + case BookieModes.EXCHANGE: { + this.props.setMode(BookieModes.EXCHANGE); + this.props.navigateTo('/exchange' + subroute); + break; + } + + case BookieModes.SPORTSBOOK: { + this.props.setMode(BookieModes.SPORTSBOOK); + this.props.navigateTo('/sportsbook' + subroute); + break; + } + + default: + break; + } + } + + render() { + return ( +
+

this.toggle(BookieModes.EXCHANGE) } + className={ this.props.bookMode === BookieModes.EXCHANGE ? 'active' : '' } + > + {I18n.t('titleBar.sportsbookToggle.exchange')} +

+ +

this.toggle(BookieModes.SPORTSBOOK) } + className={ this.props.bookMode === BookieModes.SPORTSBOOK ? 'active' : '' } + > + {I18n.t('titleBar.sportsbookToggle.sportsbook')} +

+
+ ); + } +} + +SportsbookToggle.propTypes = { + bookMode: PropTypes.string, + setMode: PropTypes.func, + navigateTo: PropTypes.func, +}; + +const mapStateToProps = (state) => { + const previousRoute = state.getIn(['routing', 'locationBeforeTransitions']); + let sportID, eventGroupID, eventID, bmgID; + + // If the previous route exists + if (previousRoute) { + let splitRoute = previousRoute.pathname.split('/'); + + // If we're deeper than the base routes + if (splitRoute.length > 3) { + // Get the object that we're currently looking at + let blockchainObject = splitRoute[splitRoute.length - 1]; + + // The object type lives in the 'y' position (x.y.z) + let objectType = blockchainObject.split('.')[1]; + + // If we've got a BMG, then we need to pull an eventID + if (objectType === ChainTypes.object_type.sport.toString()) { + sportID = blockchainObject; + } else if (objectType === ChainTypes.object_type.event_group.toString()) { + eventGroupID = blockchainObject; + } else if (objectType === ChainTypes.object_type.betting_market_group.toString()) { + // We want to have the parent eventID on hand in case the user toggles to the exchange + eventID = EventPageSelector.getEventIdByFromBMGId(state, blockchainObject); + } else if (objectType === ChainTypes.object_type.event.toString()) { + // We want to have the 'first' bmgID on hand incase the user toggles to the sportsbook + let bmg = EventPageSelector.getFirstBettingMarketGroupByEventId(state, { + eventId: blockchainObject, + }); + + if (bmg) { + bmgID = bmg.get('id'); + } + } + } + } + + return { + bookMode: state.getIn(['app', 'bookMode']), + previousRoute: state.getIn(['routing', 'previousRoute']), + sportID, eventGroupID, eventID, bmgID, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return bindActionCreators( + { + setMode: AppActions.setBookMode, + navigateTo: NavigateActions.navigateTo, + }, + dispatch + ); +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(SportsbookToggle); diff --git a/src/components/App/TitleBar/TitleBar.jsx b/src/components/App/TitleBar/TitleBar.jsx index 8514f403f..7669e8b69 100644 --- a/src/components/App/TitleBar/TitleBar.jsx +++ b/src/components/App/TitleBar/TitleBar.jsx @@ -154,6 +154,7 @@ class TitleBar extends PureComponent { onMinimizeClick={ this.onMinimizeClick } onCloseClick={ this.onCloseClick } isMaximized={ this.state.isMaximized } + loggedIn={ this.props.loggedIn } style={ style } /> ); @@ -167,6 +168,7 @@ class TitleBar extends PureComponent { onResizeClick={ this.onResizeClick } onCloseClick={ this.onCloseClick } isFullscreen={ this.state.isFullscreen } + loggedIn={ this.props.loggedIn } style={ style } /> ); @@ -181,6 +183,10 @@ TitleBar.propTypes = { style: PropTypes.object }; -const mapStateToProps = () => ({}); +const mapStateToProps = (state) => { + return { + loggedIn: state.getIn(['account', 'isLoggedIn']), + }; +}; export default connect(mapStateToProps)(TitleBar); diff --git a/src/components/App/TitleBar/WindowsTitleBar/WindowsTitleBar.jsx b/src/components/App/TitleBar/WindowsTitleBar/WindowsTitleBar.jsx index 9a5ec768e..9b92f7895 100644 --- a/src/components/App/TitleBar/WindowsTitleBar/WindowsTitleBar.jsx +++ b/src/components/App/TitleBar/WindowsTitleBar/WindowsTitleBar.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import Clock from '../Clock'; import {I18n} from 'react-redux-i18n'; import {Config} from '../../../../constants'; +import SportsbookToggle from '../SportsbookToggle'; class WindowsTitleBar extends PureComponent { render() { @@ -14,6 +15,7 @@ class WindowsTitleBar extends PureComponent { onRestoreDownClick, onCloseClick, isMaximized, + loggedIn, ...props } = this.props; @@ -26,6 +28,9 @@ class WindowsTitleBar extends PureComponent {
+ {loggedIn && ( + + )} 0){ + if (isEmpty || oddsPopulated || this.props.autoOddsPopulated > 0){ isValid = true; } else { isValid = isValidBetTotal; diff --git a/src/components/BettingDrawers/MarketDrawer/PlaceBets.jsx b/src/components/BettingDrawers/MarketDrawer/PlaceBets.jsx index 2a7a437e0..aa2f6c71e 100644 --- a/src/components/BettingDrawers/MarketDrawer/PlaceBets.jsx +++ b/src/components/BettingDrawers/MarketDrawer/PlaceBets.jsx @@ -181,7 +181,7 @@ const mapStateToProps = (state, ownProps) => { // If odds exists, it has either been provided by the user and is an incomplete bet or it has // been provided via clicking a bet from the /exchange. // If odds exists, autopopulated bets increment. - if( profit && odds && stake){ + if ( profit && odds && stake){ autoOddsPopulated = autoOddsPopulated + 1; } diff --git a/src/components/BettingDrawers/QuickBetDrawer/QuickBetDrawer.jsx b/src/components/BettingDrawers/QuickBetDrawer/QuickBetDrawer.jsx index e074d6294..da6212b22 100644 --- a/src/components/BettingDrawers/QuickBetDrawer/QuickBetDrawer.jsx +++ b/src/components/BettingDrawers/QuickBetDrawer/QuickBetDrawer.jsx @@ -213,7 +213,7 @@ const mapStateToProps = (state, ownProps) => { // If odds exists, it has either been provided by the user and is an incomplete bet or it has // been provided via clicking a bet from the /exchange. // If odds exists, autopopulated bets increment. - if( profit && odds && stake){ + if ( profit && odds && stake){ autoOddsPopulated = autoOddsPopulated + 1; } diff --git a/src/components/BettingMarketGroup/BettingMarketGroup.jsx b/src/components/BettingMarketGroup/BettingMarketGroup.jsx index fef4354c6..85b5df62c 100644 --- a/src/components/BettingMarketGroup/BettingMarketGroup.jsx +++ b/src/components/BettingMarketGroup/BettingMarketGroup.jsx @@ -11,6 +11,7 @@ import {BettingMarketGroupPageActions, MarketDrawerActions, NavigateActions} fro import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import PeerPlaysLogo from '../PeerPlaysLogo'; +import {AppUtils} from '../../utility'; import _ from 'lodash'; class BettingMarketGroup extends PureComponent { @@ -37,7 +38,7 @@ class BettingMarketGroup extends PureComponent { ) { // Betting market group doesn't exist, // Go back to home page - this.props.navigateTo('/exchange'); + this.props.navigateTo(AppUtils.getHomePath(this.props.bookMode)); } else { const prevBettingMarketGroupId = this.props.params.objectId; @@ -145,6 +146,7 @@ const mapStateToProps = (state, ownProps) => { widgetTitle: BettingMarketGroupPageSelector.getWidgetTitle(state, ownProps), rules: BettingMarketGroupPageSelector.getRules(state, ownProps), canCreateBet: MarketDrawerSelector.canAcceptBet(state, ownProps), + bookMode: state.getIn(['app', 'bookMode']), sportName }); } diff --git a/src/components/BettingWidgets/BackingBettingWidget/BackingBettingWidget.jsx b/src/components/BettingWidgets/BackingBettingWidget/BackingBettingWidget.jsx new file mode 100644 index 000000000..f3a853815 --- /dev/null +++ b/src/components/BettingWidgets/BackingBettingWidget/BackingBettingWidget.jsx @@ -0,0 +1,130 @@ +/* + * BackingBettingWidget + * + * The Backing Betting Widget has two types. Either your are rendering ... + * - An Event + * - A Betting Market Group + * + * The variable @eventFlag is set to true if the component detects and eventName and + * an eventTime. Otherwise the component assumes it is rendering a bettingMarketGroup + */ + +import React, {PureComponent} from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {MarketDrawerActions, QuickBetDrawerActions, NavigateActions} from '../../../actions'; +import PropTypes from 'prop-types'; +import BettingMarket from './BettingMarket'; +import {Col} from 'antd'; +import {SportsbookUtils} from '../../../utility'; +import {DateUtils} from '../../../utility'; +import moment from 'moment'; + +class BackingBettingWidget extends PureComponent { + render() { + // The betting markets get passed in with the marketData + const bettingMarkets = this.props.marketData.get('bettingMarkets'); + + let dateString; + + let title = this.props.title; + + let createBet = this.props.marketDrawerCreateBet; + + // The event flag will render an event if true, BMG otherwise + let eventFlag = false; + + let span = 24; + let eventTime; + + // If the following if statement is true, then the component is an event + if (this.props.eventTime && !this.props.eventRoute) { + eventFlag = true; + const localDate = moment.utc(this.props.eventTime).local(); + + dateString = DateUtils.getMonthAndDay(localDate); + createBet = this.props.quickBetDrawerCreateBet; + eventTime = localDate.format('H:mm'); + } + + return ( +
+ + { eventFlag && + + + + {dateString} +
+ {eventTime} +
+ + + +

this.props.navigateTo('/sportsbook/events/' + this.props.eventID) }> + { title } +

+ + + } + + {bettingMarkets && bettingMarkets.map((item, index) => { + + let description = item.get('description'); + + if (eventFlag && description === 'The Draw') { + description = 'Draw'; + span = 2; + } else if (eventFlag) { + span = SportsbookUtils.getColumnSize(this.props.columnType, eventFlag); + } else { + span = SportsbookUtils.getColumnSize(title, eventFlag); + + if (bettingMarkets.length === 3) { + span = 8; + } + } + + return ( + + + + ); + })} +
+ ); + } +} + +BackingBettingWidget.propTypes = { + isLiveMarket: PropTypes.bool.isRequired, +}; + +const mapDispatchToProps = (dispatch) => { + return bindActionCreators( + { + marketDrawerCreateBet: MarketDrawerActions.createBet, + quickBetDrawerCreateBet: QuickBetDrawerActions.createBet, + navigateTo: NavigateActions.navigateTo + }, + dispatch + ); +}; + +export default connect(null, mapDispatchToProps)(BackingBettingWidget); diff --git a/src/components/BettingWidgets/BackingBettingWidget/BackingBettingWidget.less b/src/components/BettingWidgets/BackingBettingWidget/BackingBettingWidget.less new file mode 100644 index 000000000..93a7c69f7 --- /dev/null +++ b/src/components/BettingWidgets/BackingBettingWidget/BackingBettingWidget.less @@ -0,0 +1,136 @@ +@import (reference) '../../App/Colors.less'; +@import (reference) '../../App/ProThemeColors.less'; + +@tileHeight: 40px; + +.backingBettingWidget { + background-color: @cinder; + padding: 0px; + display: block; + width:100%; + height: @tileHeight; + + /* Media Queries for 125-150% zoom levels */ + @media (max-width: 1400px) { + height:50px; + } + + @media (max-width: 1281px) { + height:60px; + } + + .ant-col-2 { + .backBettingMarket { + @media (max-width: 1281px) { + padding-left: 8px; + } + } + + } + + &.disabled { + .name, .date { + color: grey; + + &:hover { + color: grey; + cursor: not-allowed; + } + } + } + + .date { + font-size: 12px; + font-weight: 300; + padding: 4px 4px 4px 12px; + display: flex; + align-items: center; + + .dateString { + min-width: 38px; + } + } + + .name { + display: flex; + align-items: center; + padding: 4px 12px 4px 8px; + + &:hover { + color: @dark_tangerine; + cursor: pointer; + } + } + + .name, .date { + color: @white; + background-color: @midnight; + height: -webkit-fill-available; + line-height: 1.2; + } + + .backBettingMarket { + background-color: @midnight; + padding: 4px 16px 6px 16px; + margin: 0px 0px 4px 4px; + height: -webkit-fill-available; + line-height: 1.2; + + &.eventFlag { + display: flex; + + .odds { + margin-top: 8px; + text-align: right; + } + + .bmTitle { + margin-top: 8px; + } + } + + &.active:hover { + background-color: @cello; + cursor: pointer; + } + + &.disabled { + .odds, .bmTitle { + color: grey; + } + + &:hover { + cursor: not-allowed; + color: grey; + } + } + + .bmTitle { + width: 100%; + } + + .odds { + float: left; + width: 100%; + } + + .bmTitle, .odds { + + &.eventFlag { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + float: left; + } + + display: inline-block; + } + + .odds { + color: @anakiwa; + float: right; + font-weight: bold; + } + } +} \ No newline at end of file diff --git a/src/components/BettingWidgets/BackingBettingWidget/BettingMarket.jsx b/src/components/BettingWidgets/BackingBettingWidget/BettingMarket.jsx new file mode 100644 index 000000000..cee179d11 --- /dev/null +++ b/src/components/BettingWidgets/BackingBettingWidget/BettingMarket.jsx @@ -0,0 +1,77 @@ +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {SportsbookUtils} from '../../../utility'; + +class BettingMarket extends PureComponent { + constructor(props) { + super(props); + this.getBestOdds = this.getBestOdds.bind(this); + this.offerClicked = this.offerClicked.bind(this); + } + + getBestOdds(layBets) { + let bestOffer = layBets.last(); + + if (bestOffer) { + return bestOffer.get('odds'); + } else { + return '--'; + } + } + + offerClicked() { + // Return early if the cell was clicked and the market is not live + if (!SportsbookUtils.isAbleToBet(this.props.eventStatus)) { + return; + } + + let odds = this.getBestOdds(this.props.backOrigin); + + if (odds === '--') { + odds = 1.01; + } + + if (this.props.eventFlag) { + this.props.createBet( + this.props.eventID, + this.props.eventName, + 'back', + this.props.bettingMarketId, + odds, + ); + } else { + this.props.createBet( + 'back', + this.props.bettingMarketId, + odds + ); + } + + } + + render() { + const {title, backOrigin} = this.props; + return ( +
+
{title}
+
{this.getBestOdds(backOrigin)}
+
+ ); + } +} + +BettingMarket.propTypes = { + isLiveMarket: PropTypes.bool.isRequired, + createBet: PropTypes.func.isRequired, + bettingMarketId: PropTypes.string.isRequired, + eventFlag: PropTypes.bool +}; + +export default BettingMarket; diff --git a/src/components/BettingWidgets/BackingBettingWidget/index.js b/src/components/BettingWidgets/BackingBettingWidget/index.js new file mode 100644 index 000000000..6dc74856e --- /dev/null +++ b/src/components/BettingWidgets/BackingBettingWidget/index.js @@ -0,0 +1,3 @@ +import BackingBettingWidget from './BackingBettingWidget'; +import './BackingBettingWidget.less'; +export default BackingBettingWidget; diff --git a/src/components/BettingWidgets/BackingWidgetContainer/BackingWidgetContainer.jsx b/src/components/BettingWidgets/BackingWidgetContainer/BackingWidgetContainer.jsx new file mode 100644 index 000000000..5e80cec8c --- /dev/null +++ b/src/components/BettingWidgets/BackingWidgetContainer/BackingWidgetContainer.jsx @@ -0,0 +1,49 @@ +import React, {PureComponent} from 'react'; +import 'react-table/react-table.css'; +import {BackingBettingWidget} from '../'; + +class BackingWidgetContainer extends PureComponent { + render() { + + let eventFlag = false; + + if (this.props.marketData.size > 0) { + eventFlag = true; + } + + return ( +
+
{this.props.widgetTitle}
+ +{ !eventFlag && this.props.marketData.length > 0 && this.props.marketData.map((market, index) => { // eslint-disable-line + return (); + })} + + { eventFlag && this.props.marketData.size > 0 && // eslint-disable-line + () + } +
+ ); + } +} + +export default BackingWidgetContainer; diff --git a/src/components/BettingWidgets/BackingWidgetContainer/BackingWidgetContainer.less b/src/components/BettingWidgets/BackingWidgetContainer/BackingWidgetContainer.less new file mode 100644 index 000000000..80274291c --- /dev/null +++ b/src/components/BettingWidgets/BackingWidgetContainer/BackingWidgetContainer.less @@ -0,0 +1,16 @@ +@import (reference) '../../App/Colors.less'; +@import (reference) '../../App/ProThemeColors.less'; + +.backingWidgetContainer { + background-color: @cinder; + padding: 16px 16px 12px 16px; + margin-top: 20px; + display: inline-block; + width: 100%; + + .title { + color: @dark_tangerine; + font-size: 16px; + margin-bottom: 16px; + } +} \ No newline at end of file diff --git a/src/components/BettingWidgets/BackingWidgetContainer/index.js b/src/components/BettingWidgets/BackingWidgetContainer/index.js new file mode 100644 index 000000000..2be3ea6a7 --- /dev/null +++ b/src/components/BettingWidgets/BackingWidgetContainer/index.js @@ -0,0 +1,3 @@ +import BackingWidgetContainer from './BackingWidgetContainer'; +import './BackingWidgetContainer.less'; +export default BackingWidgetContainer; \ No newline at end of file diff --git a/src/components/BettingWidgets/SimpleBettingWidget/SimpleBettingWidget.jsx b/src/components/BettingWidgets/SimpleBettingWidget/SimpleBettingWidget.jsx index 5f8f6a11b..13b53f4fe 100644 --- a/src/components/BettingWidgets/SimpleBettingWidget/SimpleBettingWidget.jsx +++ b/src/components/BettingWidgets/SimpleBettingWidget/SimpleBettingWidget.jsx @@ -78,7 +78,7 @@ const hasOffers = (record, index) => { // This checks that there is a betting market exisiting to pair up with another. // aka: a second team betting market to be a competitor to the first one - if(index && offers.getIn([index - 1, 'betting_market_id']) === undefined) { + if (index && offers.getIn([index - 1, 'betting_market_id']) === undefined) { hasOffers = false; } diff --git a/src/components/BettingWidgets/index.js b/src/components/BettingWidgets/index.js index aecb40d55..38dddd477 100644 --- a/src/components/BettingWidgets/index.js +++ b/src/components/BettingWidgets/index.js @@ -13,4 +13,6 @@ import ComplexBettingWidget from './ComplexBettingWidget'; import SimpleBettingWidget from './SimpleBettingWidget'; -export {ComplexBettingWidget, SimpleBettingWidget}; +import BackingBettingWidget from './BackingBettingWidget'; +import BackingWidgetContainer from './BackingWidgetContainer'; +export {ComplexBettingWidget, SimpleBettingWidget, BackingBettingWidget, BackingWidgetContainer}; diff --git a/src/components/ChangePassword/ChangePassword.jsx b/src/components/ChangePassword/ChangePassword.jsx index c849af53c..dff5ec67f 100644 --- a/src/components/ChangePassword/ChangePassword.jsx +++ b/src/components/ChangePassword/ChangePassword.jsx @@ -17,6 +17,7 @@ import {NavigateActions, AuthActions} from '../../actions'; import {LoadingStatus} from '../../constants'; import Immutable from 'immutable'; import PeerPlaysLogo from '../PeerPlaysLogo'; +import {AppUtils} from '../../utility'; class ChangePassword extends PureComponent { constructor(props) { @@ -77,7 +78,7 @@ class ChangePassword extends PureComponent { * @param {object} event - the 'Home' link click event */ navigateToHome(event) { - this.navigateToLocation(event, '/exchange'); + this.navigateToLocation(event, AppUtils.getHomePath(this.props.bookMode)); } render() { @@ -141,12 +142,14 @@ class ChangePassword extends PureComponent { } const mapStateToProps = (state) => { + const bookMode = state.getIn(['app', 'bookMode']); const loadingStatus = state.getIn(['auth', 'changePasswordLoadingStatus']); const errors = loadingStatus === LoadingStatus.ERROR ? state.getIn(['auth', 'changePasswordErrors']) : Immutable.List(); return { + bookMode, loadingStatus, errors }; diff --git a/src/components/EventGroup/EventGroup.jsx b/src/components/EventGroup/EventGroup.jsx index 018c53282..16ed18e8f 100644 --- a/src/components/EventGroup/EventGroup.jsx +++ b/src/components/EventGroup/EventGroup.jsx @@ -5,7 +5,7 @@ import {SimpleBettingWidget} from '../BettingWidgets'; import {EventGroupPageActions, NavigateActions} from '../../actions'; import {EventGroupPageSelector, QuickBetDrawerSelector} from '../../selectors'; import PeerPlaysLogo from '../PeerPlaysLogo'; -import {DateUtils} from '../../utility'; +import {DateUtils, AppUtils} from '../../utility'; import {bindActionCreators} from 'redux'; const MAX_EVENT_PER_PAGE = 15; @@ -20,7 +20,7 @@ class EventGroup extends PureComponent { if (!nextProps.eventGroup || nextProps.eventGroup.isEmpty()) { // Event group doesn't exist, // Go back to home page - this.props.navigateTo('/exchange'); + this.props.navigateTo(AppUtils.getHomePath(this.props.bookMode)); } else { const prevEventGroupId = this.props.params.objectId; const nextEventGroupId = nextProps.params.objectId; @@ -76,7 +76,8 @@ const mapStateToProps = (state, ownProps) => { sportName: EventGroupPageSelector.getSportName(state, ownProps), eventGroupName: EventGroupPageSelector.getEventGroupName(state, ownProps), events: EventGroupPageSelector.getEventGroupPageData(state, ownProps), - canCreateBet: QuickBetDrawerSelector.canAcceptBet(state, ownProps) + canCreateBet: QuickBetDrawerSelector.canAcceptBet(state, ownProps), + bookMode: state.getIn(['app', 'bookMode']) }); } diff --git a/src/components/Exchange/Exchange.jsx b/src/components/Exchange/Exchange.jsx index 38a93a46a..a8c0a6fe0 100644 --- a/src/components/Exchange/Exchange.jsx +++ b/src/components/Exchange/Exchange.jsx @@ -149,7 +149,9 @@ class Exchange extends PureComponent { // Pick one of the 2 betting drawers based on the path let selectBettingDrawer = (pathTokens) => { - if (pathTokens.length < 3 || pathTokens[2].toLowerCase() !== 'bettingmarketgroup') { + if (pathTokens.length < 3 || + (pathTokens[2].toLowerCase() !== 'bettingmarketgroup' && + pathTokens[2].toLowerCase() !== 'events')) { return ; } diff --git a/src/components/HelpAndSupport/HelpAndSupport.jsx b/src/components/HelpAndSupport/HelpAndSupport.jsx index 2f2456ad4..2808573b6 100644 --- a/src/components/HelpAndSupport/HelpAndSupport.jsx +++ b/src/components/HelpAndSupport/HelpAndSupport.jsx @@ -7,6 +7,7 @@ import {NavigateActions} from '../../actions'; import Faq from './Faq'; import FaqBanner from '../../assets/images/FAQ_banner@2x.png'; import PeerPlaysLogo from '../PeerPlaysLogo'; +import {AppUtils} from '../../utility'; class HelpAndSupport extends PureComponent { constructor(props) { @@ -16,7 +17,7 @@ class HelpAndSupport extends PureComponent { //Redirect to 'Home' screen when clicked on 'Home' link on the Breadcrumb handleNavigateToHome() { - this.props.navigateTo('/exchange'); + this.props.navigateTo(AppUtils.getHomePath(this.props.bookMode)); } render() { @@ -39,6 +40,11 @@ class HelpAndSupport extends PureComponent { } } +const mapStateToProps = (state) => ({ + bookMode: state.getIn(['app', 'bookMode']) +}); + + const mapDispatchToProps = (dispatch) => bindActionCreators( { navigateTo: NavigateActions.navigateTo @@ -47,6 +53,6 @@ const mapDispatchToProps = (dispatch) => bindActionCreators( ); export default connect( - null, + mapStateToProps, mapDispatchToProps )(HelpAndSupport); diff --git a/src/components/Main/Main.jsx b/src/components/Main/Main.jsx index feae6c261..09e7834ef 100644 --- a/src/components/Main/Main.jsx +++ b/src/components/Main/Main.jsx @@ -8,6 +8,7 @@ import {withRouter} from 'react-router'; import {NavigateActions, SidebarActions, AppActions, EventActions} from '../../actions'; import {LoadingStatus} from '../../constants'; import {SidebarSelector} from '../../selectors'; +import {AppUtils} from '../../utility'; const {Content} = Layout; class Main extends PureComponent { @@ -31,7 +32,7 @@ class Main extends PureComponent { //Redirect to 'Home' screen when clicked on 'Home' link on the Breadcrumb handleNavigateToHome() { - this.props.navigateTo('/exchange'); + this.props.navigateTo(AppUtils.getHomePath(this.props.bookMode)); } onRouteChange() { @@ -71,7 +72,8 @@ const mapStateToProps = (state) => { completeTree: SidebarSelector.getSidebarCompleteTree(state), sidebarLoadingStatus: state.getIn(['sidebar', 'loadingStatus']), searchResult: event.get('searchResult'), - getSearchEventsLoadingStatus: event.get('getSearchEventsLoadingStatus') + getSearchEventsLoadingStatus: event.get('getSearchEventsLoadingStatus'), + bookMode: state.getIn(['app', 'bookMode']) }; }; diff --git a/src/components/Main/NavBar/NavBar.jsx b/src/components/Main/NavBar/NavBar.jsx index 7901f5864..0d034ac2a 100644 --- a/src/components/Main/NavBar/NavBar.jsx +++ b/src/components/Main/NavBar/NavBar.jsx @@ -8,11 +8,14 @@ * - Display notifications. */ import React, {PureComponent} from 'react'; +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; import {Layout} from 'antd'; //import SearchMenu from './SearchMenu'; import TopMenu from './TopMenu'; import logo from '../../../assets/images/bookie_logo_topnav.png'; - +import {AppUtils} from '../../../utility'; +import {NavigateActions} from '../../../actions'; const {Header} = Layout; class NavBar extends PureComponent { @@ -24,7 +27,7 @@ class NavBar extends PureComponent { //Redirect to 'Home' screen when clicked on 'Home' link on the Breadcrumb handleNavigateToHome() { - this.props.navigateTo('/exchange'); + this.props.navigateTo(AppUtils.getHomePath(this.props.bookMode)); } //Called by parent component Main @@ -34,7 +37,8 @@ class NavBar extends PureComponent { renderLogo() { //Hide cursor and deactivate click event of logo when on home page - const isHomeScreen = window.location.hash.endsWith('/exchange'); + const isHomeScreen = window.location.hash.endsWith('/exchange') || + window.location.hash.endsWith('/sportsbook'); return (
({ + bookMode: state.getIn(['app', 'bookMode']) +}); + + +const mapDispatchToProps = (dispatch) => bindActionCreators( + { + navigateTo: NavigateActions.navigateTo + }, + dispatch +); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(NavBar); + diff --git a/src/components/MyAccount/MyAccount.jsx b/src/components/MyAccount/MyAccount.jsx index d7c18f5a7..32aba543e 100644 --- a/src/components/MyAccount/MyAccount.jsx +++ b/src/components/MyAccount/MyAccount.jsx @@ -50,6 +50,8 @@ import { import {MyAccountPageSelector} from '../../selectors'; import PeerPlaysLogo from '../PeerPlaysLogo'; import {Config} from '../../constants'; +import {AppUtils} from '../../utility'; + const Option = Select.Option; @@ -216,7 +218,7 @@ class MyAccount extends PureComponent { * link on the Breadcrumb - {@link Exchange} */ handleNavigateToHome() { - this.props.navigateTo('/exchange'); + this.props.navigateTo(AppUtils.getHomePath(this.props.bookMode)); } /** @@ -400,7 +402,8 @@ const mapStateToProps = (state) => ({ availableBalance: MyAccountPageSelector.availableBalanceSelector(state), withdrawLoadingStatus: MyAccountPageSelector.withdrawLoadingStatusSelector(state), convertedAvailableBalance: MyAccountPageSelector.formattedAvailableBalanceSelector(state), - accountName: MyAccountPageSelector.accountNameSelector(state) + accountName: MyAccountPageSelector.accountNameSelector(state), + bookMode: state.getIn(['app', 'bookMode']) }); function mapDispatchToProps(dispatch) { diff --git a/src/components/MyWager/MyWager.jsx b/src/components/MyWager/MyWager.jsx index 03da48d01..7f4068016 100644 --- a/src/components/MyWager/MyWager.jsx +++ b/src/components/MyWager/MyWager.jsx @@ -39,8 +39,9 @@ import {bindActionCreators} from 'redux'; import {Map} from 'immutable'; import {I18n} from 'react-redux-i18n'; import {MyWagerSelector, MyAccountPageSelector} from '../../selectors'; -import {MyWagerTabTypes} from '../../constants'; +import {MyWagerTabTypes, BookieModes} from '../../constants'; import PeerPlaysLogo from '../PeerPlaysLogo'; +import {AppUtils} from '../../utility'; const {getBetData, getBetTotal, getCurrencyFormat, getBetsLoadingStatus} = MyWagerSelector; const TabPane = Tabs.TabPane; @@ -80,7 +81,7 @@ class MyWager extends PureComponent { /** Redirect to 'Home' screen when clicked on 'Home' link on the Breadcrumb */ onHomeLinkClick(e) { e.preventDefault(); - this.props.navigateTo('/exchange'); + this.props.navigateTo(AppUtils.getHomePath(this.props.bookMode)); } /** @@ -168,7 +169,13 @@ class MyWager extends PureComponent { * This will navigat user to event full market screen */ handleEventClick(record) { - this.props.navigateTo(`/exchange/bettingmarketgroup/${record.group_id}`); + if (this.props.bookMode === BookieModes.EXCHANGE) { + this.props.navigateTo(`/exchange/bettingmarketgroup/${record.group_id}`); + } + + if (this.props.bookMode === BookieModes.SPORTSBOOK) { + this.props.navigateTo(`/sportsbook/events/${record.event_id}`); + } } /** @@ -298,6 +305,7 @@ function filterOdds(tableData, oddsFormat) { } const mapStateToProps = (state) => ({ + bookMode: state.getIn(['app', 'bookMode']), betsData: getBetData(state), betsLoadingStatus: getBetsLoadingStatus(state), betsCurrencyFormat: getCurrencyFormat(state), diff --git a/src/components/SideBar/SideBar.jsx b/src/components/SideBar/SideBar.jsx index 6bd34c886..e9020addd 100644 --- a/src/components/SideBar/SideBar.jsx +++ b/src/components/SideBar/SideBar.jsx @@ -32,6 +32,8 @@ import PropTypes from 'prop-types'; import {SidebarSelector} from '../../selectors'; import log from 'loglevel'; import {DateUtils} from '../../utility'; +import {BookieModes} from '../../constants'; +import AppUtils from './../../utility/AppUtils'; class SideBar extends PureComponent { constructor(props) { @@ -47,7 +49,8 @@ class SideBar extends PureComponent { componentWillReceiveProps(nextProps) { if ( this.props.completeTree !== nextProps.completeTree || - this.props.objectId !== nextProps.objectId + this.props.objectId !== nextProps.objectId || + this.props.bookMode !== nextProps.bookMode ) { this.setState({ tree: this.createCurrentStateTree(nextProps.completeTree, nextProps.objectId) @@ -55,6 +58,14 @@ class SideBar extends PureComponent { } } + componentDidUpdate(prevProps) { + if (prevProps.bookMode !== this.props.bookMode) { + this.setState({ + tree: this.createCurrentStateTree(this.props.completeTree, this.props.objectId) + }); + } + } + setNodeSelected(node) { return node.set('isSelected', true).set('isOpen', true); } @@ -73,10 +84,7 @@ class SideBar extends PureComponent { * for detailed explanation. */ /* https://stackoverflow.com/questions/41298577/how-to-get-altered-tree-from-immutable-tree-maximising-reuse-of-nodes*/ //eslint-disable-line - createCurrentStateTree( - completeTree, - targetObjectId - ) { + createCurrentStateTree(completeTree, targetObjectId) { if (!targetObjectId || targetObjectId === 'exchange') { //hardcode id for all-sports node, targetObjectId = '0'; @@ -94,25 +102,28 @@ class SideBar extends PureComponent { // For sport if (keyPath.length === 1) { - newTree = newTree.updateIn(keyPath.slice(0, 1), this.setNodeSelected); + newTree = newTree.updateIn(keyPath.slice(0, 1), (node) => node.set('isSelected', true).set('isOpen', true)); // eslint-disable-line } else if (keyPath.length === 3) { // For event group newTree = newTree - .updateIn(keyPath.slice(0, 1), this.setNodeOpen) - .updateIn(keyPath.slice(0, 3), this.setNodeSelected); + .updateIn(keyPath.slice(0, 1), (node) => node.set('isOpen', true)) + .updateIn(keyPath.slice(0, 3), (node) => node.set('isSelected', true).set('isOpen', true) + ); } else if (keyPath.length === 5) { // For event newTree = newTree - .updateIn(keyPath.slice(0, 1), this.setNodeOpen) - .updateIn(keyPath.slice(0, 3), this.setNodeSelected) - .updateIn(keyPath.slice(0, 5), this.setNodeSelected); + .updateIn(keyPath.slice(0, 1), (node) => node.set('isOpen', true)) + .updateIn(keyPath.slice(0, 3), (node) => node.set('isSelected', true).set('isOpen', true)) + .updateIn(keyPath.slice(0, 5), (node) => node.set('isSelected', true).set('isOpen', true) + ); } else if (keyPath.length === 7) { // For betting market group newTree = newTree - .updateIn(keyPath.slice(0, 1), this.setNodeOpen) - .updateIn(keyPath.slice(0, 3), this.setNodeOpen) - .updateIn(keyPath.slice(0, 5), this.setNodeSelected) - .updateIn(keyPath.slice(0, 7), this.setNodeSelected); + .updateIn(keyPath.slice(0, 1), (node) => node.set('isOpen', true)) + .updateIn(keyPath.slice(0, 3), (node) => node.set('isOpen', true)) + .updateIn(keyPath.slice(0, 5), (node) => node.set('isSelected', true).set('isOpen', true)) + .updateIn(keyPath.slice(0, 7), (node) => node.set('isSelected', true).set('isOpen', true) + ); } // Compare all nodes to see which ones were altered: @@ -120,16 +131,27 @@ class SideBar extends PureComponent { const altered = differences(completeTree, newTree, 'children').map((x) => x.get('id')); if (keyPath.length >= 5) { + if (this.props.bookMode === BookieModes.SPORTSBOOK) { + // If we're in sportbook mode + // remove all the children of the event from being shown in the sidebar. + keyPath.push('children'); + newTree = newTree.removeIn(keyPath); + } + newTree = newTree.setIn( keyPath.slice(0, 4), - newTree.getIn(keyPath.slice(0, 4)).filter((metric) => metric.get('id') === altered[2]) + newTree.getIn(keyPath.slice(0, 4)).filter((metric) => { + return metric.get('id') === altered[2]; + }) ); } if (keyPath.length >= 3) { newTree = newTree.setIn( keyPath.slice(0, 2), - newTree.getIn(keyPath.slice(0, 2)).filter((metric) => metric.get('id') === altered[1]) + newTree.getIn(keyPath.slice(0, 2)).filter((metric) => { + return metric.get('id') === altered[1]; + }) ); } @@ -165,24 +187,31 @@ class SideBar extends PureComponent { * @param keyPath - is the path from root to current node */ onNodeMouseClick(event, tree, node) { - const {navigateTo} = this.props; + let navPath = (AppUtils.getHomePath(this.props.bookMode)); // '0' is hardcode id for all-sports node, if (node.id === '0') { - navigateTo('/exchange/'); + this.props.navigateTo(navPath); } else { if (node.customComponent.toLowerCase() === 'event') { + // If you're viewing a sportsbook, there is no BMG page. Stop and redirect to events page. + + if (this.props.bookMode === BookieModes.SPORTSBOOK) { + // Return early so no further code is executed. + return this.props.navigateTo(navPath + '/events/' + node.id); + } + const moneyline = node.children.filter( (mktGroup) => mktGroup.description.toUpperCase() === 'MONEYLINE' ); if (moneyline.length > 0) { - navigateTo('/exchange/bettingmarketgroup/' + moneyline[0].id); + this.props.navigateTo(navPath + '/bettingmarketgroup/' + moneyline[0].id); } else { - navigateTo('/exchange/bettingmarketgroup/' + node.children[0].id); + this.props.navigateTo(navPath + '/bettingmarketgroup/' + node.children[0].id); } } else { - navigateTo('/exchange/' + node.customComponent.toLowerCase() + '/' + node.id); + this.props.navigateTo(navPath + '/' + node.customComponent.toLowerCase() + '/' + node.id); } } } @@ -263,7 +292,8 @@ SideBar.defaultProps = { const mapStateToProps = (state) => { return { - completeTree: SidebarSelector.getSidebarCompleteTree(state) + completeTree: SidebarSelector.getSidebarCompleteTree(state), + bookMode: state.getIn(['app', 'bookMode']) }; }; diff --git a/src/components/Sport/Sport.jsx b/src/components/Sport/Sport.jsx index c03d973f1..939e148fe 100644 --- a/src/components/Sport/Sport.jsx +++ b/src/components/Sport/Sport.jsx @@ -5,7 +5,7 @@ import {SimpleBettingWidget} from '../BettingWidgets'; import {SportPageActions, NavigateActions} from '../../actions'; import {SportPageSelector, QuickBetDrawerSelector} from '../../selectors'; import PeerPlaysLogo from '../PeerPlaysLogo'; -import {DateUtils} from '../../utility'; +import {DateUtils, AppUtils} from '../../utility'; import {bindActionCreators} from 'redux'; const MAX_EVENTS_PER_WIDGET = 10; @@ -20,7 +20,7 @@ class Sport extends PureComponent { if (!nextProps.sport || nextProps.sport.isEmpty()) { // Sport doesn't exist, // Go back to home page - this.props.navigateTo('/exchange'); + this.props.navigateTo(AppUtils.getHomePath(this.props.bookMode)); } else { const prevSportId = this.props.params.objectId; const nextSportId = nextProps.params.objectId; @@ -76,9 +76,11 @@ class Sport extends PureComponent { const mapStateToProps = (state, ownProps) => { const sport = SportPageSelector.getSport(state, ownProps); + const bookMode = state.getIn(['app', 'bookMode']); let props = { - sport + sport, + bookMode }; // Populate other properties if sport exists diff --git a/src/components/SportsBook/SportsBook.jsx b/src/components/SportsBook/SportsBook.jsx new file mode 100644 index 000000000..f1addbf97 --- /dev/null +++ b/src/components/SportsBook/SportsBook.jsx @@ -0,0 +1,86 @@ +import React, {PureComponent} from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {BackingWidgetContainer} from '../BettingWidgets'; +import {EventPageSelector} from '../../selectors'; +import {ObjectUtils, SportsbookUtils} from '../../utility'; +import {NavigateActions} from '../../actions'; + +const MAX_EVENTS = 3; + +class SportsBook extends PureComponent { + render() { + + return ( +
+
+ { + this.props.allSports.map((sport) => { + + const events = sport.get('events'); + + let eventsToDisplay = []; + + events && events.slice(0, MAX_EVENTS).forEach((e) => { + + let bmgs = e.get('bettingMarketGroups'); + + if (bmgs) { + let bmg = bmgs.first(); + + if (bmg && SportsbookUtils.hasBettingMarkets(bmg)) { + eventsToDisplay.push( + bmg + .set('eventName', e.get('name')) + .set('eventID', e.get('id')) + .set('eventTime', e.get('start_time')) + .set('eventStatus', ObjectUtils.eventStatus(e)) + ); + } + } + }); + + if (eventsToDisplay.length > 0) { + return ( + + ); + } else { + return null; + } + }) + } +
+ ); + } +} + +const mapStateToProps = (state, ownProps) => { + const allSports = EventPageSelector.getAllSportsData(state, ownProps); + + return { + allSports + }; +}; + +const mapDispatchToProps = (dispatch) => bindActionCreators( + { + navigateTo: NavigateActions.navigateTo + }, + dispatch +); + +export default connect(mapStateToProps, mapDispatchToProps)(SportsBook); diff --git a/src/components/SportsBook/SportsBook.less b/src/components/SportsBook/SportsBook.less new file mode 100644 index 000000000..23f8659b6 --- /dev/null +++ b/src/components/SportsBook/SportsBook.less @@ -0,0 +1,16 @@ +@import (reference) '../App/Colors.less'; +@import (reference) '../App/ProThemeColors.less'; + +.more-sport-link { + width: 100%; + height: 30px; + background-color: @cinder; + a { + float: right; + margin-right: 15px; + color: @white; + &:hover { + color: @dark_tangerine; + } + } +} diff --git a/src/components/SportsBook/index.js b/src/components/SportsBook/index.js new file mode 100644 index 000000000..49e1548e8 --- /dev/null +++ b/src/components/SportsBook/index.js @@ -0,0 +1,3 @@ +import SportsBook from './SportsBook'; +import './SportsBook.less'; +export default SportsBook; \ No newline at end of file diff --git a/src/components/SportsBookEvent/SportsBookEvent.jsx b/src/components/SportsBookEvent/SportsBookEvent.jsx new file mode 100644 index 000000000..f13b6f892 --- /dev/null +++ b/src/components/SportsBookEvent/SportsBookEvent.jsx @@ -0,0 +1,145 @@ +import React, {PureComponent} from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {BettingMarketGroupBanner} from '../Banners'; +import {BackingWidgetContainer} from '../BettingWidgets'; +import {ObjectUtils, DateUtils} from '../../utility'; +import PeerPlaysLogo from '../PeerPlaysLogo'; +import {MarketDrawerActions, NavigateActions, BettingMarketGroupPageActions} from '../../actions'; +import _ from 'lodash'; + +import { + BettingMarketGroupPageSelector, + EventGroupPageSelector, + MarketDrawerSelector, + MyAccountPageSelector, + EventPageSelector, +} from '../../selectors'; + +class SportsBookEvent extends PureComponent { + + componentDidMount() { + const doc = document.querySelector('body'); + doc.style.minWidth = '1210px'; + doc.style.overflow = 'overlay'; + } + + componentWillUnmount() { + document.querySelector('body').style.minWidth = '1002px'; + } + + componentWillMount() { + this.props.getOpenBetsForEvent(this.props.params.eventId); + } + + componentWillUpdate(nextProps) { + if (!nextProps.event || nextProps.event.isEmpty()) { + // Betting market group doesn't exist, + // Go back to home page + this.props.navigateTo('/exchange'); + } else { + const prevEventId = this.props.params.eventId; + const nextEventId = nextProps.params.eventId; + + if ( + nextEventId !== prevEventId || + !_.isEqual(this.props.marketData.toJS(), nextProps.marketData.toJS()) || + !_.isEqual(this.props.event, nextProps.event) || + nextProps.eventName !== this.props.eventName || + !_.isEqual(nextProps.eventStatus, this.props.eventStatus) + ) { + // Get the data + this.props.getOpenBetsForEvent(this.props.params.eventId); + } + } + } + + render() { + // Return nothing if betting market group doesn't exist + if (!this.props.event || this.props.event.isEmpty() || this.props.eventStatus === null) { + return null; + } else { + return ( +
+ + + {this.props.marketData.reverse().map((e, index) => { + return ( + + ); + })} +
+ +
+
+ ); + } + } +} + +const mapStateToProps = (state, ownProps) => { + const event = EventPageSelector.getEvent(state, ownProps.params.eventId); + let sportName; + + if (event) { + sportName = EventGroupPageSelector.getSportNameFromEvent(state, event); + } + + let props = { + event, + oddsFormat: MyAccountPageSelector.oddsFormatSelector(state), + }; + + // Populate other properties if betting market group exists + if (event && !event.isEmpty()) { + _.assign(props, { + marketData: EventPageSelector.getMarketData(state, { + eventId: ownProps.params.eventId + }), + eventName: event.get('name'), + eventTime: DateUtils.getLocalDate(new Date(event.get('start_time'))), + eventStatus: ObjectUtils.eventStatus(event), + isLiveMarket: ObjectUtils.isActiveEvent(event), + unconfirmedBets: BettingMarketGroupPageSelector.getUnconfirmedBets(state, ownProps), + loadingStatus: BettingMarketGroupPageSelector.getLoadingStatus(state, ownProps), + canCreateBet: MarketDrawerSelector.canAcceptBet(state, ownProps), + sportName, + }); + } + + return props; +}; + +const mapDispatchToProps = (dispatch) => { + return bindActionCreators( + { + createBet: MarketDrawerActions.createBet, + navigateTo: NavigateActions.navigateTo, + getData: BettingMarketGroupPageActions.getData, + resetOpenBets: MarketDrawerActions.resetOpenBets, + getOpenBetsForEvent: MarketDrawerActions.getOpenBetsForEvent + }, + dispatch + ); +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(SportsBookEvent); diff --git a/src/components/SportsBookEvent/index.js b/src/components/SportsBookEvent/index.js new file mode 100644 index 000000000..450a00e68 --- /dev/null +++ b/src/components/SportsBookEvent/index.js @@ -0,0 +1,3 @@ +import SportsBookEvent from './SportsBookEvent'; + +export default SportsBookEvent; \ No newline at end of file diff --git a/src/components/SportsBookEventGroup/SportsBookEventGroup.jsx b/src/components/SportsBookEventGroup/SportsBookEventGroup.jsx new file mode 100644 index 000000000..422afb76f --- /dev/null +++ b/src/components/SportsBookEventGroup/SportsBookEventGroup.jsx @@ -0,0 +1,138 @@ +import React, {PureComponent} from 'react'; +import {connect} from 'react-redux'; +import {EventPageSelector, EventGroupPageSelector} from '../../selectors'; +import {BackingWidgetContainer} from '../BettingWidgets'; +import {SportBanner} from '../Banners'; +import {ObjectUtils} from '../../utility'; +import {Icon} from 'antd'; + +const MAX_EVENTS = 15; +class SportsBookEventGroup extends PureComponent { + constructor(props) { + super(props); + this.state = { + pagination: 0 + }; + this.setPage = this.setPage.bind(this); + } + + + setPage(page) { + this.setState({ + pagination: page + }); + } + + renderPagination(numPages) { + + // Do not render pagination if there are less than two pages. + if (numPages < 2) { + return; + } + + let pageNumbers = []; + + for (let i = 0; i < numPages; i++) { + pageNumbers.push(i); + } + + const pageNum = pageNumbers.map((page) => { + return ( +
  • this.setPage(page) } + className={ this.state.pagination === page ? 'active' : '' } + > + { page + 1 /* page starts from 0, but we want the display to start from 1 */} +
  • + ); + }); + + const prevButton = + +
  • this.setPage(this.state.pagination-1) } + className={ this.state.pagination === 0 ? 'no-click' : '' } + > + +
  • +
    ; + + const nextButton = + +
  • this.setPage(this.state.pagination+1) } + className={ this.state.pagination === numPages-1 ? 'no-click' : '' } + > + +
  • +
    ; + + return ( + + {prevButton} + {pageNum} + {nextButton} + + ); + } + + render() { + const eventGroup = this.props.eventGroup; + + if (eventGroup) { + const events = eventGroup.get('events'); + let eventsToDisplay = []; + let pagination = ''; + + if (events) { + let start = this.state.pagination * MAX_EVENTS; + let finish = (this.state.pagination + 1) * MAX_EVENTS; + let numPages = Math.ceil(events.size / MAX_EVENTS); + pagination = this.renderPagination(numPages); + + events.slice(start, finish).forEach((e) => { + let bmgs = e.get('bettingMarketGroups'); + + if (bmgs && !bmgs.isEmpty()) { + let bmg = bmgs.first(); + + eventsToDisplay.push( + bmg + .set('eventName', e.get('name')) + .set('eventID', e.get('id')) + .set('eventTime', e.get('start_time')) + .set('eventStatus', ObjectUtils.eventStatus(e)) + ); + } + }); + } + + return ( +
    + + +
      + { pagination } +
    +
    + ); + } + + return null; + } +} + +const mapStateToProps = (state, ownProps) => { + const eventGroup = EventPageSelector.getEventGroupData(state, ownProps.params.objectId); + return { + eventGroup: eventGroup, + sportName: EventGroupPageSelector.getSportName(state, ownProps) + }; +}; + +export default connect(mapStateToProps)(SportsBookEventGroup); diff --git a/src/components/SportsBookEventGroup/SportsBookEventGroup.less b/src/components/SportsBookEventGroup/SportsBookEventGroup.less new file mode 100644 index 000000000..71d6a6105 --- /dev/null +++ b/src/components/SportsBookEventGroup/SportsBookEventGroup.less @@ -0,0 +1,39 @@ +@import (reference) '../App/Colors.less'; +@import (reference) '../App/ProThemeColors.less'; + +.event-group-pagination { + background-color: @cinder; + padding: 0px 15px 15px 0px; + display: flex; + justify-content: flex-end; + font-size: 16px; + + .no-cursor { + cursor: not-allowed; + } + + .pagination-icon { + font-size: 12px; + } + + li { + display: inline-block; + padding: 5px 10px 5px 10px; + margin-right: 10px; + font-size: 16px; + + &:hover { + cursor: pointer; + color: @dark_tangerine; + } + + &.active { + background-color: @cello; + } + + &.no-click { + pointer-events: none; + } + + } +} \ No newline at end of file diff --git a/src/components/SportsBookEventGroup/index.js b/src/components/SportsBookEventGroup/index.js new file mode 100644 index 000000000..bdceda2ac --- /dev/null +++ b/src/components/SportsBookEventGroup/index.js @@ -0,0 +1,3 @@ +import SportsBookEventGroup from './SportsBookEventGroup'; +import './SportsBookEventGroup.less'; +export default SportsBookEventGroup; \ No newline at end of file diff --git a/src/components/SportsBookSport/SportsBookSport.jsx b/src/components/SportsBookSport/SportsBookSport.jsx new file mode 100644 index 000000000..429265e58 --- /dev/null +++ b/src/components/SportsBookSport/SportsBookSport.jsx @@ -0,0 +1,82 @@ +import React, {PureComponent} from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {EventPageSelector} from '../../selectors'; +import {BackingWidgetContainer} from '../BettingWidgets'; +import {SportBanner} from '../Banners'; +import {ObjectUtils} from '../../utility'; +import {NavigateActions} from '../../actions'; + +const MAX_EVENTS = 10; +class SportsBookSport extends PureComponent { + render() { + const sport = this.props.sport; + return ( +
    + { sport && } + + { + sport && this.props.sport.get('eventGroups').map((eg) => { + + const events = eg.get('events'); + + let eventsToDisplay = []; + + events && events.slice(0, MAX_EVENTS).forEach((e) => { + let bmgs = e.get('bettingMarketGroups'); + + if (bmgs) { + let bmg = bmgs.first(); + + if (bmg) { + eventsToDisplay.push( + bmg + .set('eventName', e.get('name')) + .set('eventID', e.get('id')) + .set('eventTime', e.get('start_time')) + .set('eventStatus', ObjectUtils.eventStatus(e)) + ); + } + } + }); + + return eventsToDisplay.length > 0 && + ( + + ); + }) + } +
    + ); + } +} + +const mapStateToProps = (state, ownProps) => { + return { + sport: EventPageSelector.getSportData(state, ownProps.params.objectId) + }; +}; + +const mapDispatchToProps = (dispatch) => bindActionCreators( + { + navigateTo: NavigateActions.navigateTo + }, + dispatch +); + +export default connect(mapStateToProps, mapDispatchToProps)(SportsBookSport); diff --git a/src/components/SportsBookSport/index.js b/src/components/SportsBookSport/index.js new file mode 100644 index 000000000..90ae6c158 --- /dev/null +++ b/src/components/SportsBookSport/index.js @@ -0,0 +1,3 @@ +import SportsBookSport from './SportsBookSport'; + +export default SportsBookSport; \ No newline at end of file diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js index b5e7f41f3..2c41e5e3e 100644 --- a/src/constants/ActionTypes.js +++ b/src/constants/ActionTypes.js @@ -23,6 +23,7 @@ export default { APP_SHOW_NOTIFICATION_CARD: 'APP_SHOW_NOTIFICATION_CARD', APP_SET_TITLE_BAR_TRANSPARENCY: 'APP_SET_TITLE_BAR_TRANSPARENCY', APP_SET_GATEWAY_ACCOUNT: 'APP_SET_GATEWAY_ACCOUNT', + APP_SET_BOOK_MODE: 'APP_SET_BOOK_MODE', APP_HIDE_LICENSE_SCREEN: 'APP_HIDE_LICENSE_SCREEN', // Auth actions AUTH_RESET_AUTO_LOGIN_INFO: 'AUTH_RESET_AUTO_LOGIN_INFO', diff --git a/src/constants/BackingWidgetTypes.js b/src/constants/BackingWidgetTypes.js new file mode 100644 index 000000000..2618ab5cf --- /dev/null +++ b/src/constants/BackingWidgetTypes.js @@ -0,0 +1,39 @@ +const orderPriority = { + TOP: 0, + MIDDLE: 5, + BOTTOM: 10, +}; + +// This constant object maps onto BMG descriptions produced by BOS +const BackingWidgetTypes = { + MATCHODDS: 'MATCHODDS', + MONEYLINE: 'MONEYLINE', + OVERUNDER: 'OVERUNDER', +}; + +const BackingWidgetLayouts = { + MATCHODDS: { + columns: { + eventFlag: 6, + default: 8 + }, + order: orderPriority.TOP, // Highest Priority, always show on the top. + numberOfMarkets: 3, + }, + MONEYLINE: { + columns: { + eventFlag: 7, + default: 12 + }, + order: orderPriority.TOP, // Highest Priority, always show on the top. + }, + OVERUNDER: { + columns: { + eventFlag: 12, + default: 12 + }, + order: orderPriority.MIDDLE, // Middle priority, show below the top. + }, +}; + +export {BackingWidgetTypes, BackingWidgetLayouts}; diff --git a/src/constants/BookieModes.js b/src/constants/BookieModes.js new file mode 100644 index 000000000..3a9312c2a --- /dev/null +++ b/src/constants/BookieModes.js @@ -0,0 +1,5 @@ +const BookieModes = { + EXCHANGE: 'exchange', + SPORTSBOOK: 'sportsbook', +}; +export default BookieModes; diff --git a/src/constants/LayoutConstants.js b/src/constants/LayoutConstants.js new file mode 100644 index 000000000..50478ceb6 --- /dev/null +++ b/src/constants/LayoutConstants.js @@ -0,0 +1,11 @@ +// Enumeration for Loading Status +const LayoutConstants = { + sidebarWidth: 220, + betslipWidth: 360, + splitPaneStyle: { + top: '0px', + position: 'fixed', + }, +}; + +export default LayoutConstants; diff --git a/src/constants/index.js b/src/constants/index.js index 8859e8855..46878aa0c 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -7,6 +7,7 @@ import ObjectPrefix from './ObjectPrefix'; import BetTypes from './BetTypes'; import BetCategories from './BetCategories'; import CurrencyTypes from './CurrencyTypes'; +import BookieModes from './BookieModes'; import TimeRangePeriodTypes from './TimeRangePeriodTypes'; import ChainTypes from './ChainTypes'; import AppBackgroundTypes from './AppBackgroundTypes'; @@ -18,6 +19,8 @@ import BettingMarketStatus from './BettingMarketStatus'; import BettingMarketGroupStatus from './BettingMarketGroupStatus'; import BettingDrawerStates from './BettingDrawerStates'; import BettingMarketResolutionTypes from './BettingMarketResolutionTypes'; +import BackingWidgetTypes from './BackingWidgetTypes'; +import LayoutConstants from './LayoutConstants'; export { Config, @@ -39,5 +42,8 @@ export { EventStatus, BettingMarketStatus, BettingMarketGroupStatus, - BettingMarketResolutionTypes + BettingMarketResolutionTypes, + BookieModes, + BackingWidgetTypes, + LayoutConstants, }; diff --git a/src/index.js b/src/index.js index c314bba7f..0e81671a6 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import Main from './components/Main'; import Exchange from './components/Exchange'; import AllSports from './components/AllSports'; import Sport from './components/Sport'; +import SportsBook from './components/SportsBook'; +import SportsBookEvent from './components/SportsBookEvent'; +import SportsBookEventGroup from './components/SportsBookEventGroup'; +import SportsBookSport from './components/SportsBookSport'; import EventGroup from './components/EventGroup'; import BettingMarketGroup from './components/BettingMarketGroup'; import configureStore from './store/configureStore'; @@ -112,6 +116,14 @@ const routes = ( /> + + + + + + + + diff --git a/src/reducers/AppReducer.js b/src/reducers/AppReducer.js index ac72af3d0..c0da934b7 100644 --- a/src/reducers/AppReducer.js +++ b/src/reducers/AppReducer.js @@ -1,4 +1,5 @@ -import {ActionTypes, LoadingStatus, ConnectionStatus, AppBackgroundTypes} from '../constants'; +import {ActionTypes, LoadingStatus, ConnectionStatus, AppBackgroundTypes, + BookieModes} from '../constants'; import Immutable from 'immutable'; const initialState = Immutable.fromJS({ @@ -16,7 +17,9 @@ const initialState = Immutable.fromJS({ isShowNotificationCard: false, isTitleBarTransparent: true, gatewayAccount: {}, - showLicenseScreen: false + showLicenseScreen: false, + bookMode: BookieModes.EXCHANGE, + }); export default function(state = initialState, action) { @@ -112,6 +115,12 @@ export default function(state = initialState, action) { }); } + case ActionTypes.APP_SET_BOOK_MODE: { + return state.merge({ + bookMode: action.mode, + }); + } + default: return state; } diff --git a/src/selectors/CommonSelector.js b/src/selectors/CommonSelector.js index f1ef2be5e..46d7bae53 100644 --- a/src/selectors/CommonSelector.js +++ b/src/selectors/CommonSelector.js @@ -33,8 +33,12 @@ const getRulesById = (state) => state.getIn(['rule', 'rulesById']); const getSportsById = (state) => state.getIn(['sport', 'sportsById']); +const getSportById = (state, sportId) => state.getIn(['sport', 'sportsById', sportId]); + const getEventGroupsById = (state) => state.getIn(['eventGroup', 'eventGroupsById']); +const getEventGroupById = (state, eventGroupId) => state.getIn(['eventGroup', 'eventGroupsById', eventGroupId]); // eslint-disable-line + const getEventGroupsBySportId = createSelector( getEventGroupsById, (eventGroupsById) => eventGroupsById.toList().groupBy((eventGroup) => eventGroup.get('sport_id')) @@ -229,6 +233,8 @@ const CommonSelector = { getNotificationSetting, getAssetsById, getSportsById, + getSportById, + getEventGroupById, getEventGroupsById, getEventGroupsBySportId, getEventsById, @@ -250,4 +256,4 @@ const CommonSelector = { getSimpleBettingWidgetBinnedOrderBooksByEventId }; -export default CommonSelector; +export default CommonSelector; \ No newline at end of file diff --git a/src/selectors/EventPageSelector.js b/src/selectors/EventPageSelector.js new file mode 100644 index 000000000..ee05908d2 --- /dev/null +++ b/src/selectors/EventPageSelector.js @@ -0,0 +1,393 @@ +import {createSelector} from 'reselect'; +import CommonSelector from './CommonSelector'; +import Immutable from 'immutable'; +import {SportsbookUtils} from '../utility'; +import {Config} from '../constants'; + +const { + getBettingMarketsById, + getBinnedOrderBooksByBettingMarketId, + getActiveEventsBySportId, + getSportsById, + getSportById, + getEventGroupsBySportId, + getEventGroupById, + getEventsByEventGroupId +} = CommonSelector; + +const filters = Config.filters; + +const getEvent = (state, id) => state.getIn(['event', 'eventsById', id]); + +const getEventIdByFromBMGId = (state, id) => { + const bmgs = state.getIn(['bettingMarketGroup', 'bettingMarketGroupsById']); + let foundEventID = -1; + + if (bmgs) { + bmgs.valueSeq().forEach((bettingMarket) => { + if (bettingMarket.get('id') === id) { + foundEventID = bettingMarket.get('event_id'); + } + }); + } + + return foundEventID; +}; + +const getBettingMarketGroupsByEventId = (state, ownProps) => { + const bmgs = state.getIn(['bettingMarketGroup', 'bettingMarketGroupsById']); + const eventId = ownProps.eventId; + + let bettingMarketGroups = Immutable.List(); + bmgs.valueSeq().forEach((bettingMarketGroup) => { + if (bettingMarketGroup.get('event_id') === eventId) { + bettingMarketGroups = bettingMarketGroups.push(bettingMarketGroup); + } + }); + return bettingMarketGroups; +}; + +const getAllBettingMarketGroupsByEventId = (state) => { + const bmgs = state.getIn(['bettingMarketGroup', 'bettingMarketGroupsById']); + let events = []; + bmgs.valueSeq().forEach((bmg) => { + let eventID = bmg.get('event_id'); + + if (!events[eventID]) { + events[eventID] = Immutable.List(); + } + + events[eventID] = events[eventID].push(bmg); + }); + return events; +}; + +const getFirstBettingMarketGroupByEventId = createSelector( + [getBettingMarketGroupsByEventId], + (bettingMarketGroups) => { + if (bettingMarketGroups.first()) { + return SportsbookUtils.prioritySort(bettingMarketGroups).first(); + } + } +); + +const getBettingMarketsByBMGID = createSelector([getBettingMarketsById], (bettingMarkets) => { + let bettingMarketsByGroupID = {}; + + bettingMarkets.forEach((bettingMarket) => { + const groupID = bettingMarket.get('group_id'); + + if (!bettingMarketsByGroupID[groupID]) { + bettingMarketsByGroupID[groupID] = []; + } + + bettingMarketsByGroupID[groupID].push(bettingMarket); + }); + return bettingMarketsByGroupID; +}); + +const getMarketData = createSelector( + [getBettingMarketGroupsByEventId, getBettingMarketsByBMGID, getBinnedOrderBooksByBettingMarketId], + ( + bettingMarketGroups, + // bettingMarkets is a dictionary + // - Where the keys are BMG ID's + // - Where the values are arrays that contain all of the BM's that pertain to the BMG + bettingMarkets, + binnedOrderBooksByBettingMarketId + ) => { + // Iterate through the values of the bettingMarkets dictionary + Object.values(bettingMarkets).forEach((bm) => { + // There are one or more bm in a single betting market group, this loop matches them + // with the order book that pertains to the BM. + for (let i = 0; i < bm.length; i++) { + bm[i] = bm[i].set('orderBook', binnedOrderBooksByBettingMarketId.get(bm[i].get('id'))); + + // We only care about the lay bets (the bets that will display as open back bets) + let aggregated_lay_bets = bm[i].getIn(['orderBook', 'aggregated_lay_bets']); + + if (aggregated_lay_bets) { + aggregated_lay_bets = aggregated_lay_bets.map((aggregated_lay_bet) => { + const odds = aggregated_lay_bet.get('backer_multiplier') / Config.oddsPrecision; + return aggregated_lay_bet.set('odds', odds); + }); + bm[i] = bm[i].set( + 'backOrigin', + aggregated_lay_bets.sort((a, b) => a.get('odds') - b.get('odds')) + ); + } + } + }); + + // Iterate through the list of betting market groups and... + bettingMarketGroups.forEach((bmg, index) => { + // Pick the betting markets out of the dictionary that pertain to the given BMG + bettingMarketGroups = bettingMarketGroups.set( + index, + bmg.set('bettingMarkets', bettingMarkets[bmg.get('id')]) + ); + }); + + // Group all of the over under BMGs as if they belonged to the same BMG. + bettingMarketGroups = SportsbookUtils.groupOverUnders(bettingMarketGroups); + + // Sort the betting market groups so that they are in priority order. Then center + // the draw bm in BMG's where there is a draw option + bettingMarketGroups = SportsbookUtils.sortAndCenter(bettingMarketGroups); + + return bettingMarketGroups; + } +); + +const getBettingMarketsWithOrderBook = createSelector( + [getBettingMarketsById, getBinnedOrderBooksByBettingMarketId], + (bettingMarkets, binnedOrderBooksByBettingMarketId) => { + // Iterate through the values of the bettingMarkets dictionary + bettingMarkets = bettingMarkets.map((bm) => { + // There are one or more bm in a single betting market group, this loop matches them + // with the order book that pertains to the BM. + + bm = bm.set('orderBook', binnedOrderBooksByBettingMarketId.get(bm.get('id'))); + + // We only care about the lay bets (the bets that will display as open back bets) + let aggregated_lay_bets = bm.getIn(['orderBook', 'aggregated_lay_bets']); + + if (aggregated_lay_bets) { + aggregated_lay_bets = aggregated_lay_bets.map((aggregated_lay_bet) => { + const odds = aggregated_lay_bet.get('backer_multiplier') / Config.oddsPrecision; + return aggregated_lay_bet.set('odds', odds); + }); + bm = bm.set( + 'backOrigin', + aggregated_lay_bets.sort((a, b) => a.get('odds') - b.get('odds')) + ); + } + + return bm; + }); + + let orderBooksByBMGID = []; + + bettingMarkets.forEach((bm) => { + let bmgID = bm.get('group_id'); + + if (!orderBooksByBMGID[bmgID]) { + orderBooksByBMGID[bmgID] = []; + } + + orderBooksByBMGID[bmgID].push(bm); + }); + + return orderBooksByBMGID; + } +); + +const getAllSportsData = createSelector( + [ + getSportsById, + getActiveEventsBySportId, + getAllBettingMarketGroupsByEventId, + getBettingMarketsWithOrderBook + ], + (sportsById, activeEventsBySportId, bmgsByEventID, bettingMarketsWithOrderBook) => { + let allSportsData = Immutable.List(); + + // Iterate through each sport to build each sport node + sportsById.forEach((sport) => { + // A sport node consists of a name and a sport ID + let sportNode = Immutable.Map() + .set('name', sport.get('name')) + .set('sport_id', sport.get('id')); + + // Get the events that pertain to the current sport + const activeEvents = activeEventsBySportId.get(sport.get('id')); + + // Iterate through each event and parse the relevant data + let eventNodes = + activeEvents && + activeEvents.map((e) => { + let bmgs = bmgsByEventID[e.get('id')]; + + if (bmgs) { + bmgs = bmgs.filter((bmg) => { + let description = bmg.get('description').toUpperCase(); + let passesFilters = false; + + if (filters.bettingMarketGroup.description.includes(description)) { + passesFilters = true; + } + + return passesFilters; + }).map((bmg) => { + let bmgID = bmg.get('id'); + return bmg.set('bettingMarkets', bettingMarketsWithOrderBook[bmgID]); + }); + + bmgs = SportsbookUtils.sortAndCenter(bmgs); + + // Put the list of BMGs into their respective events + e = e.set('bettingMarketGroups', bmgs); + + return e; + } else { + return null; + } + }); + + if (eventNodes) { + eventNodes = eventNodes.filter((e) => e); + eventNodes = SportsbookUtils.sortEventsByDate(eventNodes); + } + + // Set events to the sport + sportNode = sportNode.set('events', eventNodes); + // Set sport to the all sports data + allSportsData = allSportsData.push(sportNode); + }); + return allSportsData; + } +); + +const getSportData = createSelector( + [ + getSportById, + getEventGroupsBySportId, + getEventsByEventGroupId, + getAllBettingMarketGroupsByEventId, + getBettingMarketsWithOrderBook + ], + (sport, eventGroups, events, bmgsByEventID, bettingMarketsWithOrderBook) => { + if (!eventGroups || !events || !sport) { + return; + } + + let sportData = Immutable.Map(); + sportData = sportData + .set('eventGroups', eventGroups.get(sport.get('id'))) + .set('sportId', sport.get('id')) + .set('sportName', sport.get('name')); + + eventGroups = sportData.get('eventGroups').map((eg) => { + let eventList = events.get(eg.get('id')); + let eventName = eg.get('name').toUpperCase(); + + if (eventList && !filters.eventGroup.name.includes(eventName)) { + eventList = eventList.map((e) => { + if (e.get('status') !== null && e.get('status') !== undefined) { + + let bmgs = bmgsByEventID[e.get('id')]; + + if (bmgs) { + bmgs = bmgs.map((bmg) => { + let bmgID = bmg.get('id'); + return bmg.set('bettingMarkets', bettingMarketsWithOrderBook[bmgID]); + }).filter((bmg) => { + let description = bmg.get('description').toUpperCase(); + let passesFilters = false; + + if (filters.bettingMarketGroup.description.includes(description)) { + passesFilters = true; + } + + return passesFilters; + }); + + bmgs = SportsbookUtils.sortAndCenter(bmgs); + } + + // Put the list of BMGs into their respective events + return e.set('bettingMarketGroups', bmgs); + } else { + return Immutable.List(); + } + }); + + eventList = SportsbookUtils.sortEventsByDate(eventList); + + if (eventList) { + return eg.set('events', eventList.filter((e) => e)); + } else { + return Immutable.List(); + } + } else { + return Immutable.List(); + } + }); + + + sportData = sportData.set('eventGroups', eventGroups); + + return sportData; + } +); + +const getEventGroupData = createSelector( + [ + getEventGroupById, + getEventsByEventGroupId, + getAllBettingMarketGroupsByEventId, + getBettingMarketsWithOrderBook + ], + (eventGroup, events, bmgsByEventID, bettingMarketsWithOrderBook) => { + if (!eventGroup || !events || !bmgsByEventID || !bettingMarketsWithOrderBook) { + return; + } + + let eventGroupData = Immutable.Map(); + + eventGroupData = eventGroupData + .set('name', eventGroup.get('name')) + .set('id', eventGroup.get('id')); + + let eventList = events.get(eventGroup.get('id')); + + eventList = eventList.map((e) => { + let bmgs = bmgsByEventID[e.get('id')]; + + if (bmgs) { + bmgs = bmgs.map((bmg) => { + let bmgID = bmg.get('id'); + return bmg.set('bettingMarkets', bettingMarketsWithOrderBook[bmgID]); + }).filter((bmg) => { + let description = bmg.get('description').toUpperCase(); + let passesFilters = false; + + if (filters.bettingMarketGroup.description.includes(description)) { + passesFilters = true; + } + + return passesFilters; + }); + bmgs = SportsbookUtils.sortAndCenter(bmgs); + } + + // Put the list of BMGs into their respective events, only if the list is not empty. + if (!bmgs.isEmpty()) { + return e.set('bettingMarketGroups', bmgs); + } + + return null; + }); + + eventList = eventList.filter((e) => e); + + eventList = SportsbookUtils.sortEventsByDate(eventList); + + eventGroupData = eventGroupData.set('events', eventList); + + return eventGroupData; + } +); + +const EventPageSelector = { + getEvent, + getMarketData, + getBettingMarketGroupsByEventId, + getEventIdByFromBMGId, + getFirstBettingMarketGroupByEventId, + getAllSportsData, + getSportData, + getEventGroupData +}; + +export default EventPageSelector; diff --git a/src/selectors/MyWagerSelector.js b/src/selectors/MyWagerSelector.js index 9c477edb7..35b7d39be 100644 --- a/src/selectors/MyWagerSelector.js +++ b/src/selectors/MyWagerSelector.js @@ -175,6 +175,7 @@ const getExtendedBets = createSelector( return bet .set('betting_market_description', bettingMarketDescription) .set('betting_market_group_description', bettingMarketGroupDescription) + .set('event_id', eventId) .set('event_name', eventName) .set('event_time', eventTime) .set('sport_name', sportName); diff --git a/src/selectors/index.js b/src/selectors/index.js index fb39b6d71..817d4914e 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -1,21 +1,23 @@ -import MyWagerSelector from './MyWagerSelector'; -import MyAccountPageSelector from './MyAccountPageSelector'; -import SidebarSelector from './SidebarSelector'; -import BettingMarketGroupPageSelector from './BettingMarketGroupPageSelector'; import AllSportsSelector from './AllSportsSelector'; -import SportPageSelector from './SportPageSelector'; +import BettingMarketGroupPageSelector from './BettingMarketGroupPageSelector'; import EventGroupPageSelector from './EventGroupPageSelector'; +import EventPageSelector from './EventPageSelector'; import MarketDrawerSelector from './MarketDrawerSelector'; +import MyWagerSelector from './MyWagerSelector'; +import MyAccountPageSelector from './MyAccountPageSelector'; import QuickBetDrawerSelector from './QuickBetDrawerSelector'; +import SidebarSelector from './SidebarSelector'; +import SportPageSelector from './SportPageSelector'; export { + AllSportsSelector, + BettingMarketGroupPageSelector, EventGroupPageSelector, - SportPageSelector, - SidebarSelector, + EventPageSelector, + MarketDrawerSelector, MyWagerSelector, MyAccountPageSelector, - BettingMarketGroupPageSelector, - AllSportsSelector, - MarketDrawerSelector, - QuickBetDrawerSelector + QuickBetDrawerSelector, + SidebarSelector, + SportPageSelector, }; diff --git a/src/store/translations.js b/src/store/translations.js index 687f3d490..19bff8038 100644 --- a/src/store/translations.js +++ b/src/store/translations.js @@ -33,7 +33,11 @@ export const translationsObject = { }, titleBar: { title: 'BookiePro.fun', - clock: 'Local Time' + clock: 'Local Time', + sportsbookToggle: { + exchange: 'Exchange', + sportsbook: 'Sportsbook' + } }, searchMenu: { no_of_result_0: 'No results found', diff --git a/src/utility/AppUtils.js b/src/utility/AppUtils.js index cefd6adec..07b42071c 100644 --- a/src/utility/AppUtils.js +++ b/src/utility/AppUtils.js @@ -1,3 +1,6 @@ +import {BookieModes} from '../constants'; + + const AppUtils = { // Check if the running platform is windows isWindowsPlatform() { @@ -14,7 +17,25 @@ const AppUtils = { // Check if the app is running inside electron isRunningInsideElectron() { return window && window.process && window.process.type === 'renderer'; + }, + + getHomePath(mode) { + let path; + + switch (mode) { + case BookieModes.EXCHANGE: + path = '/exchange'; + break; + case BookieModes.SPORTSBOOK: + path = '/sportsbook'; + break; + default: + path = '/exchange'; + } + + return path; } + }; export default AppUtils; diff --git a/src/utility/DateUtils.js b/src/utility/DateUtils.js index 39c317121..a0fcd5490 100644 --- a/src/utility/DateUtils.js +++ b/src/utility/DateUtils.js @@ -113,6 +113,22 @@ const DateUtils = { return formatted; }, + getMonthDayAndTime(date) { + let wrappedDate = moment(date); + let formatted = wrappedDate.format('MMM D H:mm'); + + if ( + wrappedDate + .calendar() + .toLowerCase() + .indexOf('today') !== -1 + ) { + formatted = I18n.t('mybets.today'); + } + + return formatted; + }, + /** * calculate start date and end date given time range period data * diff --git a/src/utility/SportsbookUtils.js b/src/utility/SportsbookUtils.js new file mode 100644 index 000000000..ecf888c2a --- /dev/null +++ b/src/utility/SportsbookUtils.js @@ -0,0 +1,292 @@ +import {BackingWidgetTypes, BackingWidgetLayouts} from '../constants/BackingWidgetTypes'; +import Immutable from 'immutable'; +import {EventStatus} from '../constants'; +import moment from 'moment'; + +const getDescriptionAsType = (description) => { + return description + .replace(/[\/\- ]/, '') + .split(' ')[0] + .toUpperCase(); +}; + +/** + * getColumnSize() + * + * Depending on the type of the backingWidget (determined by the title), + * the UI needs to render the BM's in the widget on a column basis. + * + * The number of columns will vary from type to type with some overlap between + * widgets. + * + * @param {*} backingWidget + * @returns - An integer containing the column size with respect to ant designs column layout. + * + * @note - Antd uses a 24 column layout. This function takes that into account. The calling function + * should be able to call this function like so + */ +const getColumnSize = (type, eventFlag = false) => { + if (type) { + type = getDescriptionAsType(type); + } + + if (BackingWidgetLayouts[type]) { + return BackingWidgetLayouts[type].columns[eventFlag ? 'eventFlag' : 'default']; + } + + // The default column width for events is 7, 12 otherwise + return eventFlag ? 7 : 12; +}; + +/** + * groupOverUnders() + * + * This function examines the bmgs passed in, and groups the bmgs that belong to the + * category over/under. + * + * @param {*} bettingMarketGroups - + * @returns - A new list of wherein all over/under betting markets appear to live within + * a single BMG + */ +const groupOverUnders = (bettingMarketGroups) => { + let overUnders = Immutable.Map(); + overUnders = overUnders.set('event_id', bettingMarketGroups.first().get('event_id')); + overUnders = overUnders.set('asset_id', bettingMarketGroups.first().get('asset_id')); + overUnders = overUnders.set( + 'delay_before_settling', + bettingMarketGroups.first().get('delay_before_settling') + ); + overUnders = overUnders.set('description', 'Over/Under'); + overUnders = overUnders.set('total_matched_bets_amount', 0); + overUnders = overUnders.set('bettingMarkets', Immutable.List()); + + let overUnderBMs = []; + + let nonOverUnders = Immutable.List(); + + let newBettingMarketGroups = Immutable.List(); + + const overUnder = 'over/under'; + + // Iterate through the BMGs passed in + bettingMarketGroups.forEach((bmg) => { + // Check the current BMG's description to see if it matches over/under + let description = bmg.get('description').toLowerCase(); + + if (description.includes(overUnder)) { + // Add the list of BMs in the current BM to the list of over/unders + let bettingMarkets = bmg.get('bettingMarkets'); + + if (bettingMarkets) { + for (let i = 0; i < bmg.get('bettingMarkets').length; i++) { + overUnderBMs.push(bmg.get('bettingMarkets')[i]); + } + } + } else { + nonOverUnders = nonOverUnders.push(bmg); + } + }); + + // If there were over unders present in the bettingMarkets, + // add them to the list + if (overUnderBMs.length > 0) { + overUnders = overUnders.set('bettingMarkets', overUnderBMs.sort()); + newBettingMarketGroups = newBettingMarketGroups.push(overUnders); + } + + if (nonOverUnders.size > 0) { + nonOverUnders.forEach((bmg) => { + newBettingMarketGroups = newBettingMarketGroups.push(bmg); + }); + } + + return newBettingMarketGroups; +}; + +/** + * centerTheDraw() + * + * With respect to Match Odds, there are 3 betting markets, not just win and lose. + * "The Draw" needs to come second, or in the middle, with respect to the two other teams. + * + * @param {*} bettingMarketGroup - The bettingMarketGroup containing the two teams and a draw bm + * @returns - The bettingMarketGroup wherein the draw lives in the [1] element of the list + */ +const centerTheDraw = (bettingMarketGroup) => { + let bettingMarkets = bettingMarketGroup.get('bettingMarkets'); + const description = getDescriptionAsType(bettingMarketGroup.get('description')); + + if (bettingMarkets) { + // If there is not the correct number of markets within the BMG, then return the original object + if (bettingMarkets.length !== BackingWidgetLayouts[description].numberOfMarkets) { + return bettingMarketGroup; + } + + // Find the index that the draw is in + let drawIndex = -1; + bettingMarkets.forEach((bm, index) => { + if ( + bm + .get('description') + .replace(/\s/g, '') + .toUpperCase() === 'THEDRAW' + ) { + drawIndex = index; + } + }); + + // If the draw is not in the middle. Swap it with the middle index. + if (drawIndex !== 1) { + let temp = bettingMarkets[1]; + bettingMarkets[1] = bettingMarkets[drawIndex]; + bettingMarkets[drawIndex] = temp; + } + } + + bettingMarketGroup = bettingMarketGroup.set('bettingMarkets', bettingMarkets); + + return bettingMarketGroup; +}; + +/** + * prioritySort() + * + * Priority sort uses the layout priorities defined in the constant BackingWidgetLayouts to order + * the bettingMarketGroups inside of the bettingMarketGroups list. + * + * @param {*} bettingMarketGroups - An Immutable list containing bettingMarketGroups + * @returns - A listed sorted by the priorities defined in BackingWidgetLayouts + */ +const prioritySort = (bettingMarketGroups) => { + bettingMarketGroups = bettingMarketGroups.sort((a, b) => { + let typeA = getDescriptionAsType(a.get('description')); + let typeB = getDescriptionAsType(b.get('description')); + + if (BackingWidgetLayouts[typeA] && BackingWidgetLayouts[typeB]) { + return BackingWidgetLayouts[typeA].order > BackingWidgetLayouts[typeB].order; + } else { + return true; + } + }); + return bettingMarketGroups; +}; + +/** + * isMatchOdds() + * + * This function will determine if the betting market group is a match odds betting market group + * + * @param {*} bettingMarketGroup - The betting market group in question. + * @returns - True if the bmg is a match odds bmg. False otherwise. + */ +const isMatchodds = (bettingMarketGroup) => { + const description = getDescriptionAsType(bettingMarketGroup.get('description')); + + if (description === BackingWidgetTypes.MATCHODDS) { + return true; + } + + return false; +}; + +/** + * isMoneyline() + * + * This function will determine if the betting market group is a moneyline betting market group + * + * @param {*} bettingMarketGroup - The betting market group in question. + * @returns - True if the bmg is a moneyline bmg. False otherwise. + */ +const isMoneyline = (bettingMarketGroup) => { + const description = getDescriptionAsType(bettingMarketGroup.get('description')); + + if (description === BackingWidgetTypes.MONEYLINE) { + return true; + } + + return false; +}; + + +/** + * isInThePast() + * + * This function determines whether or not the event passed in is in the past + * + * @param {*} event - The event in question + * @returns - True if the event is in the past, false otherwise. + */ +const isInThePast = (event) => { + let today = moment(); + let date = moment(event.get('start_time')); + + if (date.isBefore(today)) { + return true; + } else { + return false; + } +}; + +const sortAndCenter = (bettingMarketGroups) => { + bettingMarketGroups = prioritySort(bettingMarketGroups); + + bettingMarketGroups.forEach((bmg) => { + // If there is a betting market group that belongs to match odds + if (isMatchodds(bmg)) { + // The draw needs to be centered + bmg = centerTheDraw(bmg); + } + }); + return bettingMarketGroups; +}; + +const isAbleToBet = (eventStatus) => { + if (eventStatus) { + switch (eventStatus[1]) { + case EventStatus.FINISHED: + case EventStatus.FROZEN: + case EventStatus.COMPLETED: + case EventStatus.SETTLED: + case EventStatus.CANCELED: + return false; + default: + return true; + } + } + + return true; +}; + +const hasBettingMarkets = (bettingMarketGroup) => { + return !!bettingMarketGroup.get('bettingMarkets'); +}; + +const sortEventsByDate = (eventList) => { + + eventList = eventList.sort((a, b) => { + if (moment(a.get('start_time')).isBefore(moment(b.get('start_time')))) { + return -1; + } else { + return 1; + } + }); + + return eventList; +}; + +const SportsbookUtils = { + centerTheDraw, + getColumnSize, + getDescriptionAsType, + groupOverUnders, + hasBettingMarkets, + isAbleToBet, + isInThePast, + isMatchodds, + isMoneyline, + prioritySort, + sortAndCenter, + sortEventsByDate +}; + +export default SportsbookUtils; diff --git a/src/utility/index.js b/src/utility/index.js index 5d5bf03de..8bc61a257 100644 --- a/src/utility/index.js +++ b/src/utility/index.js @@ -15,6 +15,7 @@ import ObjectUtils from './ObjectUtils'; import MyWagerUtils from './MyWagerUtils'; import AuthUtils from './AuthUtils'; import HelpAndSupportUtils from './HelpAndSupportUtils'; +import SportsbookUtils from './SportsbookUtils'; export { AppUtils, @@ -33,5 +34,6 @@ export { MyWagerUtils, ObjectUtils, HelpAndSupportUtils, - AuthUtils + AuthUtils, + SportsbookUtils, };