12 |
13 |
18 |
19 |
--------------------------------------------------------------------------------
/app/scripts/components/lineup_tracker/lineup_box_row.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | LineupBox = require './lineup_box.cjsx'
3 | cx = React.addons.classSet
4 | module.exports = React.createClass
5 | displayName: 'LineupBoxRow'
6 | propTypes:
7 | lineupStatus: React.PropTypes.object
8 | cycleLineupStatus: React.PropTypes.func
9 | positions: React.PropTypes.array
10 | getDefaultProps: () ->
11 | lineupStatus:
12 | pivot: 'clear'
13 | blocker1: 'clear'
14 | blocker2: 'clear'
15 | blocker3: 'clear'
16 | jammer: 'clear'
17 | render: () ->
18 |
19 | {@props.positions.map (pos) ->
20 |
21 |
22 |
23 | , this}
24 |
--------------------------------------------------------------------------------
/app/scripts/components/lineup_tracker/lineup_tracker_actions.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | AppDispatcher = require '../../dispatcher/app_dispatcher.coffee'
3 | {ActionTypes} = require '../../constants.coffee'
4 | functions = require '../../functions.coffee'
5 | cx = React.addons.classSet
6 | module.exports = React.createClass
7 | displayName: 'LineupTrackerActions'
8 | propTypes:
9 | team: React.PropTypes.object.isRequired
10 | createNextJam: () ->
11 | AppDispatcher.dispatchAndEmit
12 | type: ActionTypes.CREATE_NEXT_JAM
13 | teamId: @props.team.id
14 | jamNumber: @props.team.jams.length + 1
15 | render: () ->
16 |
17 |
18 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/scripts/components/shared/connection_status.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | AppDispatcher = require '../../dispatcher/app_dispatcher'
4 | module.exports = React.createClass
5 | displayName: 'ConnectionStatus'
6 | mixins: [React.addons.PureRenderMixin]
7 | getInitialState: () ->
8 | connected: AppDispatcher.isConnected()
9 | componentDidMount: () ->
10 | AppDispatcher.addConnectionListener @onChange
11 | componentWillUnmount: () ->
12 | AppDispatcher.removeConnectionListener @onChange
13 | onChange: () ->
14 | @setState @getInitialState()
15 | render: () ->
16 | iconClass = cx
17 | 'glyphicon': true
18 | 'glyphicon-ok-sign': @state.connected
19 | 'glyphicon-remove-sign': not @state.connected
20 | 'connection-status': true
21 | 'good-status': @state.connected
22 | 'bad-status': not @state.connected
23 |
24 |
--------------------------------------------------------------------------------
/app/scripts/components/penalty_tracker/penalty_control.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | PenaltyIndicator = require './penalty_indicator.cjsx'
3 | cx = React.addons.classSet
4 | module.exports = React.createClass
5 | displayName: 'PenaltyControl'
6 | propTypes:
7 | penaltyNumber: React.PropTypes.number
8 | skaterPenalty: React.PropTypes.object
9 | teamStyle: React.PropTypes.object.isRequired
10 | target: React.PropTypes.string.isRequired
11 | jamNumberDisplay: () ->
12 | if @props.skaterPenalty? then "Jam #{@props.skaterPenalty.jamNumber}" else "Jam"
13 | render: () ->
14 |
15 |
16 | {@jamNumberDisplay()}
17 |
18 |
25 |
--------------------------------------------------------------------------------
/app/scripts/components/scoreboard/scoreboard_ads.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | ReactCSSTransitionGroup = React.addons.CSSTransitionGroup
4 | constants = require '../../constants'
5 | module.exports = React.createClass
6 | displayName: 'ScoreboardAds'
7 | propTypes:
8 | gameState: React.PropTypes.object.isRequired
9 | componentDidMount: () ->
10 | @interval = setInterval(@cycleAd, constants.AD_DISPLAY_TIME_IN_MS)
11 | componentWillUnmount: () ->
12 | clearInterval(@interval)
13 | getInitialState: () ->
14 | adIndex: 0
15 | cycleAd: () ->
16 | @setState adIndex: (@state.adIndex + 1) % @props.gameState.ads.length
17 | render: () ->
18 | containerClass = cx
19 | 'ads': true
20 |
21 |
22 |
23 |

24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/scripts/components/scorekeeper.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | functions = require '../functions.coffee'
3 | TeamSelector = require './shared/team_selector.cjsx'
4 | JamsList = require './scorekeeper/jams_list.cjsx'
5 | Jam = require '../models/jam.coffee'
6 | Pass = require '../models/pass.coffee'
7 | cx = React.addons.classSet
8 | module.exports = React.createClass
9 | displayName: 'Scorekeeper'
10 | render: () ->
11 | awayElement =
15 | homeElement =
19 |
20 |
25 |
26 |
--------------------------------------------------------------------------------
/app/scripts/components/lineup_tracker/lineup_box.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'LineupBox'
5 | propTypes:
6 | status: React.PropTypes.string
7 | cycleLineupStatus: React.PropTypes.func
8 | getDefaultProps: () ->
9 | status: 'clear'
10 | boxContent: () ->
11 | switch @props.status
12 | when 'clear' then
13 | when null then
14 | when 'went_to_box' then '/'
15 | when 'went_to_box_and_released' then 'X'
16 | when 'injured' then
17 | when 'sat_in_box' then 'S'
18 | when 'sat_in_box_and_released' then '$'
19 | when 'continuing_penalty' then '|'
20 | when 'continuing_penalty_and_released' then '+'
21 | render: () ->
22 | buttonClass = cx
23 | 'lineup-box bt-btn': true
24 | 'injury': @props.status is 'injured'
25 |
28 |
--------------------------------------------------------------------------------
/app/scripts/components/shared/jam_summary.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | {ClockManager} = require '../../clock'
3 | module.exports = React.createClass
4 | displayName: 'JamSummary'
5 | shouldComponentUpdate: (nprops, nstate) ->
6 | _.isEqual(@props, nprops) == false
7 | componentDidMount: () ->
8 | @clockManager = new ClockManager()
9 | @clockManager.addTickListener @onTick
10 | componentWillUnmount: () ->
11 | @clockManager.removeTickListener @onTick
12 | onTick: () ->
13 | @forceUpdate()
14 | render: () ->
15 |
16 |
17 |
18 | {@props.state.replace(/_/g, ' ')}
19 |
20 |
21 |
22 |
23 |
24 | {@props.clock.display()}
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/styles/_jam-timer.scss:
--------------------------------------------------------------------------------
1 | .jam-timer {
2 | margin-bottom: 15px;
3 | .jt-label {
4 | text-transform: capitalize;
5 | font-size: 1.15em;
6 | @media (min-width: $screen-md-min) {
7 | font-size: 1.5em;
8 | }
9 | }
10 | .timeout-bars {
11 | .bar {
12 | height: 35px;
13 | @media (min-width: $screen-md-min) {
14 | height: 60px;
15 | }
16 | min-width: 16px;
17 | border: 0.8px solid $gray;
18 | margin: 2px 6px;
19 | @media (min-width: $screen-tablet){
20 | margin: 4px 6px;
21 | }
22 | border-radius: 2px;
23 | padding: 0.5em 0;
24 | @media (min-width: $screen-md-min) {
25 | font-size: 2em;
26 | padding: 0.3em 0;
27 | }
28 | &.official-review {
29 | border-radius: 20px;
30 | }
31 | &.inactive {
32 | background-color: $inactive-color !important;
33 | }
34 | &.active{
35 | animation: active-animation $animation-duration infinite;
36 | }
37 | }
38 | :first-child {
39 | border-radius: 6px;
40 | font-weight: bold;
41 | height: 19px;
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/app/scripts/components/scoreboard/scoreboard_alerts.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'ScoreboardAlerts'
5 | propTypes:
6 | gameState: React.PropTypes.object.isRequired
7 | render: () ->
8 | text = switch
9 | when @props.gameState.state is 'timeout'
10 | @props.gameState.timeout
11 | when @props.gameState.state in ['unofficial final', 'official final']
12 | @props.gameState.state
13 | teamType = switch
14 | when @props.gameState.state is 'timeout' and @props.gameState.home.isTakingTimeoutOrOfficialReview()
15 | 'home'
16 | when @props.gameState.state is 'timeout' and @props.gameState.away.isTakingTimeoutOrOfficialReview()
17 | 'away'
18 | style = @props.gameState[teamType]?.colorBarStyle
19 | columnClass = cx
20 | 'col-sm-12': not teamType?
21 | 'col-sm-6': teamType?
22 | 'col-sm-offset-6': teamType is 'away'
23 |
28 |
29 |
--------------------------------------------------------------------------------
/app/scripts/components/announcers_feed.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | FeedLineupTeam = require './announcers_feed/feed_lineup_team'
3 | FeedItem = require './announcers_feed/feed_item'
4 | module.exports = React.createClass
5 | displayName: 'AnnouncersFeed'
6 | propTypes:
7 | gameState: React.PropTypes.object.isRequired
8 | render: () ->
9 | game = @props.gameState
10 | home = game.home
11 | away = game.away
12 |
13 |
14 |
15 |
16 | Lineup
17 |
18 |
19 |
20 |
21 |
22 |
23 | Actions
24 |
25 | {@props.gameState.feed.map (item) ->
26 |
27 | }
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/styles/_header.scss:
--------------------------------------------------------------------------------
1 | header {
2 | .container-fluid {
3 | padding: 0;
4 | }
5 | .title-bar {
6 | background-color: $pink;
7 | color: $white;
8 | .btn-default {
9 | background-color: $pink;
10 | }
11 | .dropdown-menu {
12 | margin-top: 0px;
13 | border-top-width: 0px;
14 | }
15 | .btn-title-menu {
16 | padding: 2px 1px;
17 | border-color: $pink;
18 | }
19 | }
20 | .navbar {
21 | background-color: $near-black;
22 | min-height: 0;
23 | }
24 | .navbar-nav {
25 | li {
26 | &.active{
27 | opacity: 0.6;
28 | }
29 | a {
30 | padding: 4px;
31 | &:active, &:hover, &:focus {
32 | opacity: 0.6;
33 | background-color: transparent;
34 | }
35 | }
36 | }
37 | }
38 | .nav > li > a > img {
39 | width: 25px;
40 | @media (min-width: $screen-phone){
41 | width: 32px;
42 | }
43 | }
44 | .logo {
45 | .container {
46 | position: relative;
47 | }
48 | img {
49 | position: absolute;
50 | top: -25px;
51 | right: 0.5em;
52 | z-index: 1;
53 | @media (max-width: $screen-xs-max) {
54 | top: -25px;
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/scripts/components/penalty_tracker/penalty_indicator.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'PenaltyIndicator'
5 | propTypes:
6 | penaltyNumber: React.PropTypes.number.isRequired
7 | skaterPenalty: React.PropTypes.object
8 | teamStyle: React.PropTypes.object.isRequired
9 | leftEarly: React.PropTypes.bool
10 | getDefaultProps: () ->
11 | leftEarly: false
12 | displayContent: () ->
13 | if @props.skaterPenalty? and @props.skaterPenalty.penalty? then @props.skaterPenalty.penalty.code else @props.penaltyNumber
14 | getStyle: () ->
15 | @props.teamStyle if @props.skaterPenalty? and @props.penaltyNumber < 6 and not @props.skaterPenalty.penalty.egregious
16 | render: () ->
17 | containerClass = cx
18 | 'penalty-indicator text-center text-uppercase bt-box': true
19 | 'box-warning': @props.skaterPenalty? and @props.penaltyNumber is 6
20 | 'box-danger': (@props.skaterPenalty? and @props.penaltyNumber >= 7) or (@props.skaterPenalty?.penalty?.egregious)
21 |
22 | {@displayContent()}
23 | {· if @props.skaterPenalty?.sat}
24 |
--------------------------------------------------------------------------------
/app/scripts/components/shared/period_summary.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | {ClockManager} = require '../../clock'
3 | module.exports = React.createClass
4 | displayName: 'PeriodSummary'
5 | shouldComponentUpdate: (nprops, nstate) ->
6 | _.isEqual(@props, nprops) == false
7 | componentDidMount: () ->
8 | @clockManager = new ClockManager()
9 | @clockManager.addTickListener @onTick
10 | componentWillUnmount: () ->
11 | @clockManager.removeTickListener @onTick
12 | onTick: () ->
13 | @forceUpdate()
14 | render: () ->
15 |
16 |
17 |
18 | {@props.period}
19 |
20 |
21 | Jam {@props.jamNumber}
22 |
23 |
24 |
25 |
26 |
27 | {@props.clock.display()}
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/scripts/components/scorekeeper/jam_item.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | ScoreNote = require './score_note.cjsx'
3 | cx = React.addons.classSet
4 | module.exports = React.createClass
5 | displayName: 'JamItem'
6 | propTypes:
7 | jam: React.PropTypes.object.isRequired
8 | selectionHandler: React.PropTypes.func
9 | render: () ->
10 | notes = @props.jam.getNotes()
11 | jammer = @props.jam.jammer
12 | jammerNumber = if jammer? then jammer.number else
13 |
14 |
15 |
16 | {@props.jam.jamNumber}
17 |
18 |
19 |
20 |
21 | {jammerNumber}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {@props.jam.getPoints()}
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/scripts/components/titlebar.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | ConnectionStatus = require './shared/connection_status'
4 | module.exports = React.createClass
5 | displayName: "TitleBar"
6 | render: () ->
7 |
8 |
9 |
10 |
11 |
30 |
{@props.gameName}
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/scripts/shallowEqual.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2013-2015, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | *
9 | * @providesModule shallowEqual
10 | */
11 |
12 | 'use strict';
13 |
14 | /**
15 | * Performs equality by iterating through keys on an object and returning
16 | * false when any key has values which are not strictly equal between
17 | * objA and objB. Returns true when the values of all keys are strictly equal.
18 | *
19 | * @return {boolean}
20 | */
21 | function shallowEqual(objA, objB) {
22 | if (objA === objB) {
23 | return true;
24 | }
25 |
26 | if (typeof objA !== 'object' || objA === null ||
27 | typeof objB !== 'object' || objB === null) {
28 | return false;
29 | }
30 |
31 | var keysA = Object.keys(objA);
32 | var keysB = Object.keys(objB);
33 |
34 | if (keysA.length !== keysB.length) {
35 | return false;
36 | }
37 |
38 | // Test for A's keys different from B.
39 | var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);
40 | for (var i = 0; i < keysA.length; i++) {
41 | if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
42 | return false;
43 | }
44 | }
45 |
46 | return true;
47 | }
48 |
49 | module.exports = shallowEqual;
50 |
--------------------------------------------------------------------------------
/app/scripts/components/shared/skater_selector.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'SkaterSelector'
5 | propTypes:
6 | skater: React.PropTypes.object
7 | style: React.PropTypes.object
8 | injured: React.PropTypes.bool
9 | setSelectorContext: React.PropTypes.func
10 | selectHandler: React.PropTypes.func
11 | placeholder: React.PropTypes.string
12 | target: React.PropTypes.string
13 | getDefaultProps: () ->
14 | target: "#skater-selector-modal"
15 | buttonContent: () ->
16 | if @props.skater
17 | @props.skater.number
18 | else
19 | {@props.placeholder}
20 | render: () ->
21 | style = @props.style if @props.skater and not @props.injured and not @props.skater.fouledOut() and not @props.skater.expelled()
22 | buttonClass = cx
23 | 'bt-btn': true
24 | 'btn-selector': not @props.skater?.fouledOut() and not @props.skater?.expelled() and not @props.injured
25 | 'btn-danger': @props.skater?.fouledOut() or @props.skater?.expelled()
26 | 'btn-injury': @props.injured
27 | 'selected': @props.skater?
28 |
--------------------------------------------------------------------------------
/app/styles/_penalty-tracker.scss:
--------------------------------------------------------------------------------
1 | .penalty-tracker, .penalty-whiteboard {
2 | .penalty-indicator {
3 | position: relative;
4 | white-space: nowrap;
5 | color: $gray;
6 | .dot {
7 | position: absolute;
8 | bottom: -0.4em;
9 | font-size: 2em;
10 | left: 50%;
11 | transform: translateX(-50%);
12 | }
13 | }
14 | .penalty-alert {
15 | @extend .penalty-indicator;
16 | }
17 | .penalty-control {
18 | .jam-number {
19 | color: $near-black;
20 | font-size: 0.6em;
21 | text-align: center;
22 | }
23 | .penalty-control-button {
24 | padding: 0;
25 | width: 100%;
26 | border: none;
27 | background: none;
28 | }
29 | }
30 | .edit-penalty {
31 | $column-offset: 100%/7.0;
32 | &.penalty-1 {
33 | @include border-arrow($column-offset * 0.5)
34 | }
35 | &.penalty-2 {
36 | @include border-arrow($column-offset * 1.5)
37 | }
38 | &.penalty-3 {
39 | @include border-arrow($column-offset * 2.5)
40 | }
41 | &.penalty-4 {
42 | @include border-arrow($column-offset * 3.5)
43 | }
44 | &.penalty-5 {
45 | @include border-arrow($column-offset * 4.5)
46 | }
47 | &.penalty-6 {
48 | @include border-arrow($column-offset * 5.5)
49 | }
50 | &.penalty-0 {
51 | @include border-arrow($column-offset * 6.5)
52 | }
53 | .jam-number-button {
54 | color: $gray;
55 | background: none;
56 | border: none;
57 | padding: 0;
58 | width: 100%;
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/app/styles/base/_variables.scss:
--------------------------------------------------------------------------------
1 | // This file will contain modification to bootstrap variables for changes relevant to Bouttime UI
2 | // For documentation on bootstrap variables see: http://getbootstrap.com/customize/
3 | // Global Changes
4 | @media only screen and (max-width: 768px) {
5 | $border-radius-base: 0px;
6 | $border-radius-large: 4px;
7 | $border-radius-small: 2px;
8 | $padding-base-vertical: 5px;
9 | $padding-base-horizontal: 1px;
10 | }
11 | // This will prevent navbar getting collapsed for extra small screen sizes:
12 | $grid-float-breakpoint: 320px;
13 | // Typography
14 | $font-size-base: 16px;
15 | // Panels
16 | $panel-body-padding: 5px;
17 | // Design variables
18 | $minimum-touch-target: 42px;
19 | $animation-duration: 2s;
20 | //Bootstrap overrides
21 | $grid-gutter-width : 16px;
22 | $padding-base-horizontal: 8px !default;
23 | $icon-font-path: "/fonts/";
24 | $font-family-sans-serif: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif;
25 | $gray-base: $near-black;
26 | $input-color: $near-black;
27 | $btn-default-color: $near-black;
28 | $btn-default-bg: $light-gray;
29 | $btn-default-border: $btn-default-bg;
30 | $btn-primary-color: $white;
31 | $btn-primary-bg: $near-black;
32 | $btn-primary-border: $btn-primary-bg;
33 | $btn-warning-color: $near-black;
34 | $btn-warning-bg: $alert-yellow;
35 | $btn-warning-border: $btn-warning-bg;
36 | $btn-danger-color: $white;
37 | $btn-danger-bg: $dark-red;
38 | $btn-danger-border: $btn-danger-bg;
39 | $btn-selected-color: $white;
40 | $btn-selected-bg: $muted-gray;
41 | $btn-selected-border: $btn-selected-bg;
--------------------------------------------------------------------------------
/test/components/scorekeeper-test.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | Scorekeeper = require '../../app/scripts/components/scorekeeper'
3 | TeamSelector = require '../../app/scripts/components/shared/team_selector'
4 | JamsList = require '../../app/scripts/components/scorekeeper/jams_list'
5 | TestUtils = React.addons.TestUtils
6 | DemoData = require '../../app/scripts/demo_data'
7 | Promise = require 'bluebird'
8 | describe 'Scorekeeper', () ->
9 | gameState = null
10 | setSelectorContext = (team, jam, func) ->
11 | [team, jam, func]
12 | scorekeeper = null
13 | beforeEach () ->
14 | gameState = DemoData.init()
15 | scorekeeper = gameState.then (gameState) ->
16 | shallowRenderer = TestUtils.createRenderer()
17 | shallowRenderer.render
18 | shallowRenderer.getRenderOutput()
19 | it 'renders a component', () ->
20 | scorekeeper.then (scorekeeper) ->
21 | expect(TestUtils.isDOMComponent(scorekeeper))
22 | it 'renders a team selector with jams lists', () ->
23 | Promise.join gameState, scorekeeper, (gameState, scorekeeper) ->
24 | teamSelector = scorekeeper.props.children
25 | expect(TestUtils.isElementOfType(teamSelector, TeamSelector)).toBe(true)
26 | expect(TestUtils.isElementOfType(teamSelector.props.awayElement, JamsList)).toBe(true)
27 | expect(TestUtils.isElementOfType(teamSelector.props.homeElement, JamsList)).toBe(true)
28 | expect(teamSelector.props.awayElement.props.team).toBe(gameState.away)
29 | expect(teamSelector.props.homeElement.props.team).toBe(gameState.home)
30 |
--------------------------------------------------------------------------------
/test/components/penalty_tracker-test.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | PenaltyTracker = require '../../app/scripts/components/penalty_tracker'
3 | TeamSelector = require '../../app/scripts/components/shared/team_selector'
4 | TeamPenalties = require '../../app/scripts/components/penalty_tracker/team_penalties'
5 | TestUtils = React.addons.TestUtils
6 | DemoData = require '../../app/scripts/demo_data'
7 | Promise = require 'bluebird'
8 | describe 'PenaltyTracker', () ->
9 | gameState = null
10 | setSelectorContext = (team, jam, func) ->
11 | [team, jam, func]
12 | penaltyTracker = null
13 | beforeEach () ->
14 | gameState = DemoData.init()
15 | penaltyTracker = gameState.then (gameState) ->
16 | shallowRenderer = TestUtils.createRenderer()
17 | shallowRenderer.render
18 | shallowRenderer.getRenderOutput()
19 | it 'renders a component', () ->
20 | penaltyTracker.then (penaltyTracker) ->
21 | expect(TestUtils.isDOMComponent(penaltyTracker))
22 | it 'renders a team selector with team penalties', () ->
23 | Promise.join gameState, penaltyTracker, (gameState, penaltyTracker) ->
24 | teamSelector = penaltyTracker.props.children
25 | expect(TestUtils.isElementOfType(teamSelector, TeamSelector)).toBe(true)
26 | expect(TestUtils.isElementOfType(teamSelector.props.awayElement, TeamPenalties)).toBe(true)
27 | expect(TestUtils.isElementOfType(teamSelector.props.homeElement, TeamPenalties)).toBe(true)
28 | expect(teamSelector.props.awayElement.props.team).toBe(gameState.away)
29 | expect(teamSelector.props.homeElement.props.team).toBe(gameState.home)
30 |
--------------------------------------------------------------------------------
/app/scripts/models/skater.coffee:
--------------------------------------------------------------------------------
1 | functions = require '../functions'
2 | _ = require 'underscore'
3 | AppDispatcher = require '../dispatcher/app_dispatcher'
4 | {ActionTypes} = require '../constants'
5 | Store = require './store'
6 | class Skater extends Store
7 | @dispatchToken: AppDispatcher.register (action) =>
8 | switch action.type
9 | when ActionTypes.SET_PENALTY
10 | @find(action.skaterId).then (skater) ->
11 | skater.setPenalty(action.jamNumber, action.penalty)
12 | skater.save()
13 | when ActionTypes.CLEAR_PENALTY
14 | @find(action.skaterId).then (skater) ->
15 | skater.clearPenalty(action.skaterPenaltyIndex)
16 | skater.save()
17 | when ActionTypes.UPDATE_PENALTY
18 | @find(action.skaterId).then (skater) ->
19 | skater.updatePenalty(action.skaterPenaltyIndex, action.opts)
20 | skater.save()
21 | constructor: (options={}) ->
22 | super options
23 | @teamId = options.teamId
24 | @name = options.name
25 | @number = options.number
26 | @penalties = options.penalties ? []
27 | setPenalty: (jamNumber, penalty) ->
28 | @penalties.push
29 | penalty: penalty
30 | jamNumber: jamNumber
31 | sat: false
32 | clearPenalty: (skaterPenaltyIndex) ->
33 | @penalties.splice(skaterPenaltyIndex, 1)
34 | updatePenalty: (skaterPenaltyIndex, opts={}) ->
35 | skaterPenalty = @penalties[skaterPenaltyIndex]
36 | _.extend(skaterPenalty, opts)
37 | expelled: () ->
38 | @penalties.some (skaterPenalty) ->
39 | skaterPenalty.penalty.egregious
40 | fouledOut: () ->
41 | @penalties.length >= 7
42 | leftEarly: () ->
43 | false
44 | module.exports = Skater
45 |
--------------------------------------------------------------------------------
/app/scripts/components/announcers_feed/feed_lineup_team.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'FeedLineupTeam'
5 | propTypes:
6 | team: React.PropTypes.object.isRequired
7 | jam: React.PropTypes.object
8 | render: () ->
9 | team = @props.team
10 | jam = @props.jam
11 | jammerClass = cx
12 | 'glyphicon glyphicon-star': jam?.jammer? and not jam?.starPass
13 | pivotClass = cx
14 | 'glyphicon glyphicon-minus-sign': jam?.pivot? and not jam?.noPivot and not jam?.starPass
15 | 'glyphicon glyphicon-star': jam?.pivot? and jam?.starPass
16 |
17 |
18 | {team.initials}
19 |
20 |
21 | {jam?.jammer?.number}
22 |
23 |
24 | {jam?.pivot?.number}
25 |
26 |
27 | {jam?.blocker1?.number}
28 |
29 |
30 | {jam?.blocker2?.number}
31 |
32 |
33 | {jam?.blocker3?.number}
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/scripts/components/penalty_box_timer.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | TeamSelector = require './shared/team_selector'
3 | TeamPenaltyTimers = require './penalty_box_timer/team_penalty_timers'
4 | PeriodSummary = require './shared/period_summary'
5 | JamSummary = require './shared/jam_summary'
6 | Team = require '../models/team.coffee'
7 | cx = React.addons.classSet
8 | module.exports = React.createClass
9 | displayName: 'PenaltyBoxTimer'
10 | render: () ->
11 | home = @props.gameState.home
12 | away = @props.gameState.away
13 | homeElement =
17 | awayElement =
21 |
41 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | ecmaFeatures:
2 | modules: true
3 | jsx: true
4 |
5 | env:
6 | amd: true
7 | browser: true
8 | es6: true
9 | jquery: true
10 | node: true
11 |
12 | extends: "eslint:recommended"
13 |
14 | rules:
15 | # 0 = off, 1 = warning, 2 = error
16 | # Best Practices
17 | complexity: [2, 20]
18 | curly: [2, all]
19 | eqeqeq: 2
20 | guard-for-in: 2
21 | no-alert: 2
22 | no-caller: 2
23 | no-div-regex: 2
24 | no-eq-null: 2
25 | no-eval: 2
26 | no-extend-native: 2
27 | no-extra-bind: 2
28 | no-implied-eval: 2
29 | no-iterator: 2
30 | no-lone-blocks: 2
31 | no-loop-func: 2
32 | no-multi-spaces: 2
33 | no-native-reassign: 2
34 | no-new-func: 2
35 | no-new-wrappers: 2
36 | no-new: 2
37 | no-octal-escape: 2
38 | no-proto: 2
39 | no-redeclare: 2
40 | no-return-assign: 2
41 | no-script-url: 2
42 | no-self-compare: 2
43 | no-unused-expressions: 2
44 | no-useless-call: 2
45 | no-useless-concat: 2
46 | no-void: 2
47 | no-with: 2
48 | radix: 2
49 |
50 | # Variables
51 | no-catch-shadow: 2
52 | no-label-var: 2
53 | no-shadow-restricted-names: 2
54 | no-undef-init: 2
55 |
56 | # Node.js and CommonJS
57 | callback-return: 2
58 | global-require: 2
59 | handle-callback-err: 2
60 | no-path-concat: 2
61 | no-process-exit: 2
62 |
63 | # Stylistic Issues
64 | array-bracket-spacing: 1
65 | block-spacing: [1, never]
66 | brace-style: 1
67 | camelcase: 1
68 | comma-dangle: [1, never]
69 | comma-spacing: 1
70 | comma-style: [2, last]
71 | computed-property-spacing: [1, never]
72 | eol-last: 1
73 | indent: [1, 2]
74 | key-spacing: 1
75 | linebreak-style: 1
76 | max-statements: [1, 30]
77 | no-mixed-spaces-and-tabs: 2
78 | no-trailing-spaces: 1
79 | semi: 1
80 |
--------------------------------------------------------------------------------
/app/scripts/components/lineup_tracker.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | TeamSelector = require './shared/team_selector'
3 | TeamLineup = require './lineup_tracker/team_lineup'
4 | LineupSelector = require './lineup_tracker/lineup_selector'
5 | Jam = require '../models/jam.coffee'
6 | cx = React.addons.classSet
7 | module.exports = React.createClass
8 | displayName: 'LineupTracker'
9 | componentDidMount: () ->
10 | Jam.addChangeListener @onChange
11 | componentWillUnmount: () ->
12 | Jam.removeChangeListener @onChange
13 | onChange: () ->
14 | Jam.find @state.lineupSelectorContext.jam?.id
15 | .then (jam) =>
16 | @setSelectorContext(
17 | @state.lineupSelectorContext.team,
18 | jam,
19 | @state.lineupSelectorContext.selectHandler)
20 | getInitialState: () ->
21 | lineupSelectorContext:
22 | team: null
23 | jam: null
24 | selectHandler: null
25 | setSelectorContext: (team, jam, selectHandler) ->
26 | @setState
27 | lineupSelectorContext:
28 | team: team
29 | jam: jam
30 | selectHandler: selectHandler
31 | #React callbacks
32 | render: () ->
33 | awayElement =
36 | homeElement =
39 |
40 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/app/scripts/components/scorekeeper/jam_details.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | PassesList = require './passes_list.cjsx'
3 | cx = React.addons.classSet
4 | module.exports = React.createClass
5 | displayName: 'JamDetails'
6 | propType:
7 | jam: React.PropTypes.object.isRequired
8 | setSelectorContext: React.PropTypes.func.isRequired
9 | mainMenuHandler: React.PropTypes.func
10 | prevJamHandler: React.PropTypes.func
11 | nextJamHandler: React.PropTypes.func
12 | render: () ->
13 |
14 |
15 |
16 |
19 |
20 |
21 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Jam {@props.jam.jamNumber}
37 |
38 |
39 | {@props.jam.getPoints()}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/scripts/components/penalty_tracker/penalties_list.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'PenaltiesList'
5 | render: () ->
6 | containerClass = cx
7 | 'penalties-list': true
8 | 'hidden': @props.hidden
9 |
10 | {@props.penalties[0...-1].map((penalty, penaltyIndex) ->
11 |
12 |
13 |
16 |
17 |
18 |
21 |
22 |
23 | , this).map((elem, i, elems) ->
24 | if i % 2 then null else
{elems[i..i+1]}
25 | ).filter (elem) ->
26 | elem?
27 | }
28 |
29 |
30 |
33 |
34 |
35 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/scripts/components/shared/team_selector.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'TeamSelector'
5 | propTypes:
6 | away: React.PropTypes.object.isRequired
7 | awayElement: React.PropTypes.element.isRequired
8 | home: React.PropTypes.object.isRequired
9 | homeElement: React.PropTypes.element.isRequired
10 | selectTeam: (teamType) ->
11 | @setState(selectedTeam: teamType)
12 | containerClass: (teamType) ->
13 | cx
14 | 'col-sm-6 col-xs-12': true
15 | 'hidden-xs': @state.selectedTeam != teamType
16 | getInitialState: () ->
17 | selectedTeam: 'away'
18 | render: () ->
19 | displayBoth = window.matchMedia('(min-width: 768px)').matches
20 | awayStyle = if @state.selectedTeam is 'away' or displayBoth then @props.away.colorBarStyle else {}
21 | homeStyle = if @state.selectedTeam is 'home' or displayBoth then @props.home.colorBarStyle else {}
22 |
23 |
24 |
25 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 | {@props.awayElement}
38 |
39 |
40 | {@props.homeElement}
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/scripts/components/penalty_box_timer/team_penalty_timers.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | PenaltyClock = require './penalty_clock'
3 | AppDispatcher = require '../../dispatcher/app_dispatcher.coffee'
4 | {ActionTypes} = require '../../constants.coffee'
5 | cx = React.addons.classSet
6 | module.exports = React.createClass
7 | displayName: 'TeamPenaltyTimers'
8 | propTypes:
9 | team: React.PropTypes.object.isRequired
10 | jamNumber: React.PropTypes.number.isRequired
11 | setSelectorContext: React.PropTypes.func.isRequired
12 | toggleAllPenaltyTimers: () ->
13 | AppDispatcher.dispatchAndEmit
14 | type: ActionTypes.TOGGLE_ALL_PENALTY_TIMERS
15 | teamId: @props.team.id
16 | render: () ->
17 | anyRunning = @props.team.anyPenaltyTimerRunning()
18 | playPauseCS = cx({
19 | 'glyphicon' : true
20 | 'glyphicon-play' : not anyRunning
21 | 'glyphicon-pause' : anyRunning
22 | })
23 |
24 |
25 |
26 |
30 |
31 |
32 |
36 |
37 |
38 |
39 | {@props.team.seats.map (seat) ->
40 |
43 | , this}
44 |
45 |
--------------------------------------------------------------------------------
/app/scripts/components/scoreboard/scoreboard_clocks.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | {ClockManager} = require '../../clock'
3 | module.exports = React.createClass
4 | displayName: 'ScoreboardClocks'
5 | componentDidMount: () ->
6 | @clockManager = new ClockManager()
7 | @clockManager.addTickListener @onTick
8 | componentWillUnmount: () ->
9 | @clockManager.removeTickListener @onTick
10 | onTick: () ->
11 | @forceUpdate()
12 | render: () ->
13 | periodNumber = switch @props.gameState.period
14 | when 'period 1' then '1'
15 | when 'period 2' then '2'
16 | when 'pregame' then 'Pre'
17 | when 'halftime' then 'Half'
18 | when 'unofficial final' then 'UF'
19 | when 'official final' then 'OF'
20 | else ''
21 | jamLabel = @props.gameState.state.replace /_/g, ' '
22 |
23 |
24 |
25 |
26 |
27 |
{periodNumber}
28 |
29 |
30 |
31 |
{@props.gameState.jamNumber}
32 |
33 |
34 |
35 |
36 |
37 |
{@props.gameState.periodClock.display()}
38 |
39 |
40 |
41 |
{@props.gameState.jamClock.display()}
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/scripts/components/scoreboard.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | $ = require 'jquery'
3 | Jam = require '../models/jam.coffee'
4 | ScoreboardClocks = require './scoreboard/scoreboard_clocks'
5 | ScoreboardTeam = require './scoreboard/scoreboard_team'
6 | ScoreboardAlerts = require './scoreboard/scoreboard_alerts'
7 | ScoreboardAds = require './scoreboard/scoreboard_ads'
8 | cx = React.addons.classSet
9 | module.exports = React.createClass
10 | displayName: 'Scoreboard'
11 | render: () ->
12 | awayJam = @props.gameState.getCurrentJam(@props.gameState.away)
13 | homeJam = @props.gameState.getCurrentJam(@props.gameState.home)
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
{homeJam?.getPoints() ? 0}
23 |
24 |
25 |
{awayJam?.getPoints() ? 0}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {if @props.gameState.state in ['timeout', 'official final', 'unofficial final']
34 |
35 | else
36 |
37 | }
38 |
39 |
40 |
--------------------------------------------------------------------------------
/test/models/box_entry-test.coffee:
--------------------------------------------------------------------------------
1 | jest.mock '../../app/scripts/models/skater'
2 | {ActionTypes} = require '../../app/scripts/constants'
3 | describe 'BoxEntry', () ->
4 | process.setMaxListeners(0)
5 | AppDispatcher = undefined
6 | BoxEntry = undefined
7 | callback = undefined
8 | beforeEach () ->
9 | AppDispatcher = require '../../app/scripts/dispatcher/app_dispatcher'
10 | BoxEntry = require '../../app/scripts/models/box_entry'
11 | callback = AppDispatcher.register.mock.calls[0][0]
12 | it 'registers a callback with the dispatcher', () ->
13 | expect(AppDispatcher.register.mock.calls.length).toBe(1)
14 | pit 'initializes with no items', () ->
15 | BoxEntry.all().then (boxes) ->
16 | expect(boxes.length).toBe(0)
17 | describe "actions", () ->
18 | box = undefined
19 | beforeEach () ->
20 | box = BoxEntry.new().tap BoxEntry.save
21 | pit "sets the box skater", () ->
22 | box.then (box) ->
23 | callback
24 | type: ActionTypes.SET_PENALTY_BOX_SKATER
25 | boxId: box.id
26 | skaterId: 'skater 2'
27 | .then (box) ->
28 | expect(box.skater.id).toBe('skater 2')
29 | pit "toggles left early", () ->
30 | box.then (box) ->
31 | callback
32 | type: ActionTypes.TOGGLE_LEFT_EARLY
33 | boxId: box.id
34 | .then (box) ->
35 | expect(box.leftEarly).toBe(true)
36 | pit "toggles served", () ->
37 | box.then (box) ->
38 | callback
39 | type: ActionTypes.TOGGLE_PENALTY_SERVED
40 | boxId: box.id
41 | .then (box) ->
42 | expect(box.served).toBe(true)
43 | pit "toggles the penalty timer", () ->
44 | box.then (box) ->
45 | callback
46 | type: ActionTypes.TOGGLE_PENALTY_TIMER
47 | boxId: box.id
48 | .then (box) ->
49 | expect(box.clock.toggle).toBeCalled()
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for helping!
4 |
5 | When contributing to this repository, please first discuss the change you wish
6 | to make via issue, email, or any other method with the owners of this
7 | repository before making a change.
8 |
9 | Please note we have a [Code of Conduct](CODE_OF_CONDUCT.md), please follow it
10 | in all your interactions with the project.
11 |
12 | Development should be done on an independent branch -- when ready, make a pull
13 | request to `master` as described below.
14 |
15 | For more help on how to contribute please reference [GitHub
16 | Help](https://help.github.com). There are many guides including howtos on
17 | working with git.
18 |
19 | ## The Seven Rules of a Great git Commit Message
20 | Simple guidelines for better commit messages [source](http://chris.beams.io/posts/git-commit/)
21 |
22 | - The seven rules of a great git commit message
23 | - Keep in mind: This has all been said before.
24 | - Separate subject from body with a blank line
25 | - Limit the subject line to 50 characters
26 | - Capitalize the subject line
27 | - Do not end the subject line with a period
28 | - Use the imperative mood in the subject line
29 | - Wrap the body at 72 characters
30 | - Use the body to explain what and why vs. how
31 |
32 | ## Pull Request Process
33 |
34 | * Verify that your branch passes all tests locally (`npm test`).
35 |
36 | * Update README.md with details of any changes to install instructions or
37 | dependencies.
38 |
39 | * For non-trivial changes, a version update maybe required. Please note this in your Pull Request so it can be discussed. A version change will require updates in the following files:
40 |
41 | - `package.json`
42 | - `bower.json`
43 | - `bin/bouttime-server`
44 |
45 | * Pull Requests must pass several checks before merging. At this moment, that
46 | requires passing tests, adhering to the style guide, and getting code review
47 | approval.
48 |
--------------------------------------------------------------------------------
/app/scripts/components/penalty_tracker/penalties_summary.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | PenaltyIndicator = require './penalty_indicator.cjsx'
3 | PenaltyAlert = require './penalty_alert.cjsx'
4 | cx = React.addons.classSet
5 | module.exports = React.createClass
6 | displayName: 'PenaltiesSummary'
7 | propTypes:
8 | team: React.PropTypes.object.isRequired
9 | selectionHandler: React.PropTypes.func
10 | hidden: React.PropTypes.bool
11 | getDefaultProps: () ->
12 | selectionHandler: () ->
13 | hidden: false
14 | getLineup: () ->
15 | jam = @props.team.jams[@props.team.jams.length-1]
16 | positions = [jam.pivot, jam.blocker1, jam.blocker2, jam.blocker3, jam.jammer]
17 | positions.filter (position) ->
18 | position?
19 | inLineup: (skater) ->
20 | skater.number in @getLineup().map (s) -> s.number
21 | isInjured: (skater) ->
22 | false
23 | render: () ->
24 | containerClass = cx
25 | 'penalties-summary': true
26 | 'hidden': @props.hidden
27 |
28 | {@props.team.skaters.map (skater, skaterIndex) ->
29 |
30 |
31 |
34 |
35 | {[0...7].map (i) ->
36 |
42 | , this}
43 |
46 |
47 | , this}
48 |
--------------------------------------------------------------------------------
/test/components/lineup_tracker-test.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | LineupTracker = require '../../app/scripts/components/lineup_tracker'
3 | TeamSelector = require '../../app/scripts/components/shared/team_selector'
4 | LineupSelector = require '../../app/scripts/components/lineup_tracker/lineup_selector'
5 | TeamLineup = require '../../app/scripts/components/lineup_tracker/team_lineup'
6 | TestUtils = React.addons.TestUtils
7 | DemoData = require '../../app/scripts/demo_data'
8 | Promise = require 'bluebird'
9 | describe 'LineupTracker', () ->
10 | gameState = null
11 | setSelectorContext = (team, jam, func) ->
12 | [team, jam, func]
13 | lineupTracker = null
14 | beforeEach () ->
15 | gameState = DemoData.init()
16 | lineupTracker = gameState.then (gameState) ->
17 | shallowRenderer = TestUtils.createRenderer()
18 | shallowRenderer.render
19 | shallowRenderer.getRenderOutput()
20 | pit 'renders a component', () ->
21 | lineupTracker.then (lineupTracker) ->
22 | expect(TestUtils.isDOMComponent(lineupTracker))
23 | pit 'renders a team selector with team lineups', () ->
24 | Promise.join gameState, lineupTracker, (gameState, lineupTracker) ->
25 | teamSelector = lineupTracker.props.children[0]
26 | expect(TestUtils.isElementOfType(teamSelector, TeamSelector)).toBe(true)
27 | expect(TestUtils.isElementOfType(teamSelector.props.awayElement, TeamLineup)).toBe(true)
28 | expect(TestUtils.isElementOfType(teamSelector.props.homeElement, TeamLineup)).toBe(true)
29 | expect(teamSelector.props.awayElement.props.team).toBe(gameState.away)
30 | expect(teamSelector.props.homeElement.props.team).toBe(gameState.home)
31 | pit 'renders a lineup selector', () ->
32 | lineupTracker.then (lineupTracker) ->
33 | lineupSelector = lineupTracker.props.children[1]
34 | expect(TestUtils.isElementOfType(lineupSelector, LineupSelector)).toBe(true)
35 |
--------------------------------------------------------------------------------
/app/scripts/components/shared/skater_selector_modal.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'SkaterSelectorModal'
5 | propTypes:
6 | team: React.PropTypes.object
7 | jam: React.PropTypes.object
8 | selectHandler: React.PropTypes.func
9 | getLineup: () ->
10 | jam = @props.jam
11 | positions = [jam.pivot, jam.blocker1, jam.blocker2, jam.blocker3, jam.jammer]
12 | positions.filter (position) ->
13 | position?
14 | inLineup: (skater) ->
15 | if @props.jam?
16 | skater.number in @getLineup().map (s) -> s.number
17 | else
18 | false
19 | isInjured: (skater) ->
20 | @props.team.skaterIsInjured(skater.id, @props.jam.jamNumber)
21 | buttonClass: (skater) ->
22 | cx
23 | 'bt-btn skater-selector-dialog-btn': true
24 | 'btn-injury': @isInjured(skater)
25 | render: () ->
26 |
27 |
28 |
29 |
30 |
31 |
Select Skater
32 |
33 |
34 | {@props.team?.skaters?.map (skater, skaterIndex) ->
35 |
43 | , this}
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/scripts/components/game_setup/roster_fields.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'RosterFields'
5 | handleNameChange: (skater, evt) ->
6 | @props.actions.updateSkater skater, name: evt.target.value
7 | handleNumberChange: (skater, evt) ->
8 | @props.actions.updateSkater skater, number: evt.target.value
9 | getInitialState: () ->
10 | skaters: []
11 | render: () ->
12 |
13 |
Roster
14 | {@props.teamState.skaters.map (skater, skaterIndex) ->
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | , this}
35 |
36 |
--------------------------------------------------------------------------------
/app/scripts/components/scoreboard/scoreboard_team.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'ScoreboardTeam'
5 | propTypes:
6 | team: React.PropTypes.object.isRequired
7 | jam: React.PropTypes.object
8 | render: () ->
9 | leadClass = cx
10 | 'glyphicon': true
11 | 'glyphicon-star': true
12 | 'hidden': not @props.jam?.passes?[0]?.lead or @props.jam?.passes?.some (pass) -> pass.lostLead
13 | officialReviewClass = cx
14 | 'official-review': true
15 | 'timeout-bar': true
16 | 'active': @props.team.isTakingOfficialReview
17 | 'inactive': @props.team.hasOfficialReview == false
18 | timeoutClass = (num) => cx
19 | 'timeout-bar': true
20 | 'timeout': true
21 | 'active': @props.team.isTakingTimeout && @props.team.timeouts is 3 - num
22 | 'inactive': @props.team.timeouts < 4 - num
23 |
24 |
25 |

26 |
27 |
{@props.team.name}
28 |
29 |
{@props.team.getPoints()}
30 |
31 |
32 |
33 |
34 |
{if @props.jam?.jammer then "#{@props.jam.jammer.number} #{@props.jam.jammer.name}"}
35 |
36 |
42 |
43 |
--------------------------------------------------------------------------------
/app/server.coffee:
--------------------------------------------------------------------------------
1 | config = require('./scripts/config')
2 | module.exports = start: (port=3000) ->
3 | config.socketUrl = "localhost:#{port}"
4 | config.server = true
5 | express = require('express')
6 | app = express();
7 | http = require('http').Server(app);
8 | io = require('socket.io')(http);
9 | constants = require('./scripts/constants')
10 | GameState = require('./scripts/models/game_state')
11 | AppDispatcher = require('./scripts/dispatcher/app_dispatcher')
12 | Exporter = require './scripts/util/exporter'
13 | {ActionTypes} = require './scripts/constants'
14 | app.get '/export/:id', (req, res) ->
15 | game = GameState.find(req.params.id).then (game) ->
16 | res.setHeader 'Content-disposition', "attachment; filename=#{game.getDisplayName()}.json"
17 | res.json Exporter.export(game)
18 | app.use '/', express.static(__dirname)
19 | games = GameState.all()
20 | GameState.addChangeListener () ->
21 | games = GameState.all()
22 | io.on 'connection', (socket) ->
23 | console.log('a user connected')
24 | socket.emit 'connected'
25 | games.map (game) ->
26 | game.getMetadata()
27 | .then (metadata) ->
28 | socket.emit 'app dispatcher',
29 | type: ActionTypes.SYNC_GAMES
30 | games: metadata
31 | socket.on 'disconnect', () ->
32 | console.log('user disconnected')
33 | socket.on 'app dispatcher', (action) ->
34 | AppDispatcher.dispatch(action)
35 | socket.broadcast.emit 'app dispatcher', action
36 | socket.on 'sync clocks', () ->
37 | timeX = new Date().getTime()
38 | setTimeout( ()->
39 | timeY = new Date().getTime()
40 | socket.emit 'clocks synced',
41 | timeX: timeX
42 | timeY: timeY
43 | ,constants.CLOCK_SYNC_DELAY_DURATION_IN_MS)
44 | socket.on 'sync game', (payload) ->
45 | GameState.find(payload.gameId).then (game) ->
46 | socket.emit 'app dispatcher',
47 | type: ActionTypes.SAVE_GAME
48 | gameState: game
49 | http.listen port, () ->
50 | console.log("listening on *:#{port}")
51 |
--------------------------------------------------------------------------------
/app/scripts/components/scorekeeper/pass_item.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | $ = require 'jquery'
3 | functions = require '../../functions.coffee'
4 | AppDispatcher = require '../../dispatcher/app_dispatcher.coffee'
5 | {ActionTypes} = require '../../constants.coffee'
6 | SkaterSelector = require '../shared/skater_selector.cjsx'
7 | ScoreNote = require './score_note.cjsx'
8 | cx = React.addons.classSet
9 | module.exports = React.createClass
10 | displayName: 'PassItem'
11 | propTypes:
12 | pass: React.PropTypes.object.isRequired
13 | panelId: React.PropTypes.string.isRequired
14 | setSelectorContext: React.PropTypes.func.isRequired
15 | setJammer: (skaterId) ->
16 | AppDispatcher.dispatchAndEmit
17 | type: ActionTypes.SET_PASS_JAMMER
18 | passId: @props.pass.id
19 | skaterId: skaterId
20 | hidePanels: () ->
21 | $('.scorekeeper .collapse.in').collapse('hide');
22 | render: () ->
23 | notes = @props.pass.getNotes()
24 | jammer = @props.pass.jammer
25 |
26 |
27 |
28 | {@props.pass.passNumber}
29 |
30 |
31 |
32 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {@props.pass.points ? 0}
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/scripts/components/game_setup.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | $ = require 'jquery'
3 | GameForm = require './game_setup/game_form'
4 | Skater = require '../models/skater'
5 | AppDispatcher = require '../dispatcher/app_dispatcher'
6 | {ActionTypes} = require '../constants'
7 | module.exports = React.createClass
8 | displayName: 'GameSetup'
9 | componentWillMount: () ->
10 | @actions =
11 | updateGame: (gameState) =>
12 | @setState(gameState: $.extend(@state.gameState, gameState))
13 | updateOfficial: (idx, official) =>
14 | @state.gameState.officials[idx] = official
15 | @setState @state
16 | addOfficial: (gameState) =>
17 | gameState.officials.push ''
18 | @setState @state
19 | removeOfficial: (gameState, officialIndex) =>
20 | gameState.officials.splice officialIndex, 1
21 | @setState @state
22 | addAd: (gameState, ad) =>
23 | gameState.ads.push ad
24 | @setState @state
25 | removeAd: (gameState, adIndex) =>
26 | gameState.ads.splice adIndex, 1
27 | @setState @state
28 | updateTeam: (team, newTeam) =>
29 | team = $.extend(team, newTeam)
30 | @setState(@state)
31 | addSkater: (team) =>
32 | team.addSkater new Skater()
33 | @setState(@state)
34 | removeSkater: (team, skater) =>
35 | team.removeSkater(skater)
36 | @setState(@state)
37 | updateSkater: (skater, newSkater) =>
38 | skater = $.extend(skater, newSkater)
39 | @setState(@state)
40 | saveGame: () =>
41 | AppDispatcher.dispatchAndEmit
42 | type: ActionTypes.SAVE_GAME
43 | gameState: @state.gameState
44 | @props.onSave()
45 | getInitialState: () ->
46 | @dirty = false
47 | gameState: $.extend(true, {}, @props.gameState)
48 | reloadState: () ->
49 | @dirty = true
50 | componentWillReceiveProps: (nextProps) ->
51 | if @dirty
52 | @dirty = false
53 | @setState gameState: $.extend(true, {}, nextProps.gameState)
54 | render: () ->
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/scripts/components/lineup_tracker/lineup_selector.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | displayName: 'LineupSelector'
5 | propTypes:
6 | team: React.PropTypes.object
7 | jam: React.PropTypes.object
8 | selectHandler: React.PropTypes.func
9 | isSelected: (position, skater) ->
10 | @props?.jam?[position]?.id is skater.id
11 | getStyle: (position, skater) ->
12 | if @isSelected(position, skater)
13 | @props.team.colorBarStyle
14 | btnClass: (skater) -> cx
15 | 'bt-btn': true
16 | 'btn-danger': skater.fouledOut() or skater.expelled()
17 | 'btn-injury': @props.team.skaterIsInjured(skater.id, @props.jam.jamNumber)
18 | render: () ->
19 |
20 |
21 |
22 |
23 |
24 |
Select Lineup
25 |
26 |
27 |
28 | {@props.jam?.listPositionLabels()?.map (pos) ->
29 |
30 | {pos}
31 |
32 | , this}
33 |
34 |
35 | {@props.jam?.listPositions()?.map (position) ->
36 |
37 | {@props.team?.skaters?.map (skater, skaterIndex) ->
38 |
44 | , this}
45 |
46 | , this}
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://circleci.com/gh/WFTDA/bouttime)
2 | [](https://codeclimate.com/github/WFTDA/bouttime)
3 |
4 | # Prerequisites
5 |
6 | BoutTime requires version 0.10.x of [node.js](https://nodejs.org/) to run, which
7 | can be installed from their website. Note this is an older version and we are
8 | working on bringing the application up to date.
9 |
10 | Alternatively, you may install node on OSX via [Homebrew](http://brew.sh/)
11 |
12 | $ brew install nvm
13 | $ export NVM_DIR="$HOME/.nvm"
14 | $. "/usr/local/opt/nvm/nvm.sh"
15 | $ nvm install 0.10.48
16 |
17 | # Using the Alpha Release
18 |
19 | You can install the current alpha release via NPM
20 |
21 | ---
22 | ### WARNING!
23 |
24 | Installing via NPM is not currently recommended.
25 | We recommend [installing for local development](#development)
26 |
27 | ---
28 |
29 | With the BoutTime server running in the background, open a browser to
30 | `http://localhost:3000` to use the app.
31 |
32 | **Note:** At this present moment, the software is considered alpha so we are
33 | discouraging regular installations in favor of local development installs.
34 | Please bear with us as we bring the app up to a releaseable state.
35 |
36 | # Development
37 |
38 | Use `setup.sh` (i.e. `./setup.sh`) to install the necessary dependencies or
39 | follow the steps below to get your development environment up and running:
40 |
41 | $ npm install -g bower gulp
42 | $ npm install
43 | $ npm link # Link from your globally installed node modules
44 |
45 | `gulp watch` will automatically recompile assets when source files are changed
46 |
47 | $ gulp watch # Build and watch for changes with gulp
48 |
49 | Start the server via `bouttime-server` or `nodemon bin/bouttime-server` to
50 | listen for changes during development and navigate to `http://localhost:3000`.
51 |
52 | ## Publishing
53 |
54 | Publish package to the NPM repository:
55 |
56 | $ # Bump version numbers
57 | $ gulp package
58 | $ git commit -am "Version bump"
59 | $ git tag alpha.x.y.z #where alpha.x.y.z is the major,minor,version
60 | $ git push origin master --tags
61 | $ npm publish
62 |
--------------------------------------------------------------------------------
/app/scripts/models/store.coffee:
--------------------------------------------------------------------------------
1 | _ = require 'underscore'
2 | functions = require '../functions'
3 | config = require '../config'
4 | {EventEmitter} = require 'events'
5 | Promise = require 'bluebird'
6 | Datastore = require 'nedb'
7 | CHANGE_EVENT = 'STORE_CHANGE'
8 | class Store
9 | @stores = {}
10 | @emitter: new EventEmitter()
11 | @_dbOpts: () ->
12 | if config.server
13 | filename: "db/#{@name}.db"
14 | autoload: true
15 | @_store: () ->
16 | store = @stores[@name]
17 | if not store?
18 | store = new Datastore(@_dbOpts())
19 | Promise.promisifyAll(store)
20 | @stores[@name] = store
21 | store
22 | @find: (id) ->
23 | @_store().findOneAsync _id: id
24 | .then (doc) =>
25 | if doc? then new this(doc) else null
26 | .tap @load
27 | @findOrCreate: (opts={}) ->
28 | @_store().findOneAsync _id: opts?.id
29 | .then (doc) =>
30 | if doc? then new this(doc) else new this(opts)
31 | .tap @load
32 | @findBy: (query={}) ->
33 | @_store().findAsync query
34 | .map (doc) =>
35 | this.new(doc)
36 | @findByOrCreate: (query, opts) ->
37 | @_store().findAsync query
38 | .then (docs) ->
39 | if docs.length > 0
40 | docs
41 | else if opts?
42 | opts.map (opt) ->
43 | _.extend(opt, query)
44 | else
45 | []
46 | .map (args) =>
47 | this.new(args)
48 | @all: () ->
49 | @findBy()
50 | @new: (opts={}) ->
51 | Promise.resolve(new this(opts)).tap @load
52 | @emitChange: () =>
53 | @emitter.emit(CHANGE_EVENT)
54 | @addChangeListener: (callback) ->
55 | @emitter.on(CHANGE_EVENT, callback)
56 | @removeChangeListener: (callback) ->
57 | @emitter.removeListener(CHANGE_EVENT, callback)
58 | @save: (obj) ->
59 | obj?.save()
60 | @load: (obj) ->
61 | obj?.load()
62 | @destroy: (obj) ->
63 | obj?.destroy()
64 | constructor: (options={}) ->
65 | @id = options.id ? functions.uniqueId()
66 | @_id = @id
67 | load: () ->
68 | Promise.resolve(this)
69 | save: (cascade=false, emit=true) ->
70 | @constructor._store().updateAsync({_id: @_id}, this, {upsert: true})
71 | .then () =>
72 | @constructor.emitChange() if emit
73 | .return this
74 | destroy: () ->
75 | @constructor._store().removeAsync({_id: @_id}, {})
76 | .then @constructor.emitChange
77 | .return this
78 | module.exports = Store
79 |
--------------------------------------------------------------------------------
/app/scripts/models/box_entry.coffee:
--------------------------------------------------------------------------------
1 | AppDispatcher = require '../dispatcher/app_dispatcher'
2 | {ActionTypes} = require '../constants'
3 | {ClockManager} = require '../clock'
4 | Store = require './store'
5 | Skater = require './skater'
6 | PENALTY_CLOCK_SETTINGS =
7 | time: 0
8 | tickUp: true
9 | class BoxEntry extends Store
10 | @dispatchToken: AppDispatcher.register (action) =>
11 | switch action.type
12 | when ActionTypes.TOGGLE_LEFT_EARLY
13 | @find(action.boxId)
14 | .tap (box) =>
15 | box.toggleLeftEarly()
16 | .tap @handleDirty
17 | .tap @save
18 | when ActionTypes.TOGGLE_PENALTY_SERVED
19 | @find(action.boxId)
20 | .tap (box) =>
21 | box.toggleServed()
22 | .tap @handleDirty
23 | .tap (box) ->
24 | if box.touch
25 | box.destroy()
26 | else
27 | box.save()
28 | when ActionTypes.SET_PENALTY_BOX_SKATER
29 | @find(action.boxId)
30 | .tap (box) =>
31 | box.setSkater(action.skaterId)
32 | .tap @handleDirty
33 | .tap @save
34 | when ActionTypes.TOGGLE_PENALTY_TIMER
35 | @find(action.boxId)
36 | .tap (box) =>
37 | box.togglePenaltyTimer()
38 | .tap @handleDirty
39 | .tap @save
40 | @handleDirty: (box) ->
41 | if not box.dirty
42 | box.dirty = true
43 | constructor: (options={}) ->
44 | super options
45 | @_clockManager = new ClockManager()
46 | @teamId = options.teamId
47 | @leftEarly = options.leftEarly ? false
48 | @served = options.served ? false
49 | @position = options.position ? 'blocker'
50 | @skater = options.skater
51 | @clock = @_clockManager.getOrAddClock(@id, options.clock ? PENALTY_CLOCK_SETTINGS)
52 | @dirty = options.dirty ? false
53 | @sort = options.sort ? 0
54 | load: () ->
55 | if @skater
56 | Skater.new(@skater).then (skater) =>
57 | @skater = skater
58 | .return(this)
59 | toggleLeftEarly: () ->
60 | @leftEarly = not @leftEarly
61 | toggleServed: () ->
62 | @served = not @served
63 | togglePenaltyTimer: () ->
64 | @clock.toggle()
65 | startPenaltyTimer: () ->
66 | @clock.start()
67 | stopPenaltyTimer: () ->
68 | @clock.stop()
69 | penaltyTimerIsRunning: () ->
70 | @clock.isRunning
71 | setSkater: (skaterId) ->
72 | Skater.find(skaterId).then (skater) =>
73 | @skater = skater
74 | module.exports = BoxEntry
75 |
76 |
77 |
--------------------------------------------------------------------------------
/test/models/skater-test.coffee:
--------------------------------------------------------------------------------
1 | constants = require '../../app/scripts/constants'
2 | ActionTypes = constants.ActionTypes
3 | MemoryStorage = require '../../app/scripts/memory_storage'
4 | describe 'Skater', () ->
5 | process.setMaxListeners(0)
6 | AppDispatcher = undefined
7 | Skater = undefined
8 | callback = undefined
9 | beforeEach () ->
10 | AppDispatcher = require '../../app/scripts/dispatcher/app_dispatcher'
11 | Skater = require '../../app/scripts/models/skater'
12 | callback = AppDispatcher.register.mock.calls[0][0]
13 | it 'registers a callback with the dispatcher', () ->
14 | expect(AppDispatcher.register.mock.calls.length).toBe(1)
15 | pit 'initializes with no items', () ->
16 | Skater.all().then (skaters) ->
17 | expect(skaters.length).toBe(0)
18 | describe "actions", () ->
19 | skater = undefined
20 | beforeEach () ->
21 | skater = Skater.new(name: 'Test Cancer', number: '42')
22 | .tap Skater.save
23 | describe "penalties", () ->
24 | beforeEach () ->
25 | skater = skater.then (skater) ->
26 | callback
27 | type: ActionTypes.SET_PENALTY
28 | skaterId: skater.id
29 | jamNumber: 1
30 | penalty: {foo: 'bar'}
31 | pit "creates a new penalty", () ->
32 | skater.then (skater) ->
33 | expect(skater.penalties.length).toBe(1)
34 | expect(skater.penalties[0]).toEqual
35 | jamNumber: 1
36 | sat: false
37 | penalty: {foo: 'bar'}
38 | pit "updates a penalty", () ->
39 | skater.then (skater) ->
40 | callback
41 | type: ActionTypes.UPDATE_PENALTY
42 | skaterId: skater.id
43 | skaterPenaltyIndex: 0
44 | opts:
45 | jamNumber: 2
46 | penalty:
47 | foo: 'baz'
48 | egregious: true
49 | sat: true
50 | .then (skater) ->
51 | expect(skater.penalties[0]).toEqual
52 | jamNumber: 2
53 | penalty:
54 | foo: 'baz'
55 | egregious: true
56 | sat: true
57 | expect(skater.expelled()).toBe(true)
58 | pit "clears a penalty", () ->
59 | skater.then (skater) ->
60 | callback
61 | type: ActionTypes.CLEAR_PENALTY
62 | skaterId: skater.id
63 | skaterPenaltyIndex: 0
64 | .then (skater) ->
65 | expect(skater.penalties.length).toBe(0)
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/scripts/components/penalty_tracker/team_penalties.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | $ = require 'jquery'
3 | AppDispatcher = require '../../dispatcher/app_dispatcher.coffee'
4 | {ActionTypes} = require '../../constants.coffee'
5 | PenaltiesSummary = require './penalties_summary.cjsx'
6 | SkaterPenalties = require './skater_penalties.cjsx'
7 | PenaltiesList = require './penalties_list.cjsx'
8 | cx = React.addons.classSet
9 | module.exports = React.createClass
10 | displayName: 'TeamPenalties'
11 | propTypes:
12 | gameState: React.PropTypes.object.isRequired
13 | team: React.PropTypes.object.isRequired
14 | getInitialState: () ->
15 | selectedSkaterId: null
16 | editingPenaltyIndex: null
17 | selectSkater: (skaterId) ->
18 | @setState(selectedSkaterId: skaterId)
19 | editPenalty: (penaltyIndex) ->
20 | @setState(editingPenaltyIndex: penaltyIndex)
21 | backHandler: () ->
22 | $('.edit-penalty.collapse.in').collapse('hide')
23 | @selectSkater(null)
24 | setPenalty:(skaterId, penalty) ->
25 | AppDispatcher.dispatchAndEmit
26 | type: ActionTypes.SET_PENALTY
27 | skaterId: skaterId
28 | jamNumber: @props.gameState.jamNumber
29 | penalty: penalty
30 | changePenalty: (skaterId, skaterPenaltyIndex, penalty) ->
31 | AppDispatcher.dispatchAndEmit
32 | type: ActionTypes.UPDATE_PENALTY
33 | skaterId: skaterId
34 | skaterPenaltyIndex: skaterPenaltyIndex
35 | opts:
36 | penalty: penalty
37 | setOrUpdatePenalty: (skaterId, penaltyIndex) ->
38 | penalty = @props.gameState.penalties[penaltyIndex]
39 | if @state.editingPenaltyIndex?
40 | @changePenalty(skaterId, @state.editingPenaltyIndex, penalty)
41 | else
42 | @setPenalty(skaterId, penalty)
43 | render: () ->
44 |
45 |
49 | {@props.team.skaters.map (skater, skaterIndex) ->
50 |
57 | , this}
58 |
62 |
--------------------------------------------------------------------------------
/app/scripts/components/scorekeeper/passes_list.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | functions = require '../../functions'
3 | AppDispatcher = require '../../dispatcher/app_dispatcher.coffee'
4 | {ActionTypes} = require '../../constants.coffee'
5 | ItemRow = require '../shared/item_row'
6 | PassItem = require './pass_item'
7 | PassEditPanel = require './pass_edit_panel'
8 | module.exports = React.createClass
9 | displayName: 'PassesList'
10 | propTypes:
11 | jam: React.PropTypes.object.isRequired
12 | setSelectorContext: React.PropTypes.func.isRequired
13 | reorderHandler: (sourceIndex, targetIndex) ->
14 | AppDispatcher.dispatchAndEmit
15 | type: ActionTypes.REORDER_PASS
16 | jamId: @props.jam.id
17 | sourcePassIndex: sourceIndex
18 | targetPassIndex: targetIndex
19 | removeHandler: (passId) ->
20 | AppDispatcher.dispatchAndEmit
21 | type: ActionTypes.REMOVE_PASS
22 | passId: passId
23 | createNextPass: () ->
24 | AppDispatcher.dispatchAndEmit
25 | type: ActionTypes.CREATE_NEXT_PASS
26 | jamId: @props.jam.id
27 | passNumber: @props.jam.passes.length + 1
28 | render: () ->
29 |
30 |
31 |
32 | Pass
33 |
34 |
35 | Skater
36 |
37 |
38 | Notes
39 |
40 |
41 | Points
42 |
43 |
44 | {@props.jam.passes.map (pass, passIndex) ->
45 | args =
46 | pass: pass
47 | jam: @props.jam
48 | panelId: "edit-pass-#{functions.uniqueId()}"
49 | setSelectorContext: @props.setSelectorContext
50 | item =
51 | panel =
52 |
59 | , this}
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/scripts/components/shared/item_row.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | $ = require 'jquery'
4 | module.exports = React.createClass
5 | displayName: 'ItemRow'
6 | propTypes:
7 | item: React.PropTypes.node.isRequired
8 | removeHandler: React.PropTypes.func.isRequired
9 | panel: React.PropTypes.node
10 | index: React.PropTypes.number
11 | reorderHandler: React.PropTypes.func
12 | getInitialState: () ->
13 | opened: false
14 | preventDefault: (evt) ->
15 | evt.preventDefault()
16 | mouseDownHandler: (evt) ->
17 | @target = evt.target
18 | dragHandler: (evt) ->
19 | if @props.reorderHandler? and @props.index?
20 | if $(@target).hasClass('drag-handle') or $(@target).parents('.drag-handle').length > 0
21 | evt.dataTransfer.setData 'passIndex', @props.index
22 | else
23 | evt.preventDefault()
24 | dropHandler: (evt) ->
25 | if @props.reorderHandler? and @props.index?
26 | sourceIndex = evt.dataTransfer.getData 'passIndex'
27 | @props.reorderHandler(sourceIndex, @props.index)
28 | removeHandler: () ->
29 | if window.confirm("Do you really want to remove this item? This action cannot be undone and affects other interfaces")
30 | @props.removeHandler()
31 | toggleOpened: () ->
32 | @setState(opened: not @state.opened)
33 | render: () ->
34 | containerClass = cx
35 | 'item-row': true
36 | 'opened': @state.opened
37 | handleClass = cx
38 | 'bt-btn options-button': true
39 | 'drag-handle': @props.reorderHandler? and @props.index?
40 |
47 |
48 |
49 |
52 |
53 | {@props.item}
54 |
55 |
56 |
57 |
58 |
61 |
62 |
63 |
64 |
65 |
66 | {@props.panel if @props.panel?}
67 |
68 |
--------------------------------------------------------------------------------
/app/scripts/components/game_picker.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | AppDispatcher = require '../dispatcher/app_dispatcher'
3 | GameState = require '../models/game_state'
4 | GameMetadata = require '../models/game_metadata'
5 | GameSetup = require './game_setup'
6 | Game = require './game'
7 | qs = require 'querystring'
8 | cx = React.addons.classSet
9 | module.exports = React.createClass
10 | displayName: 'GamePicker'
11 | getInitialState: () ->
12 | selectedGame: @parseSelectedGame()
13 | games: []
14 | newGame: null
15 | parseSelectedGame: () ->
16 | qs.parse(window?.location?.hash?.substring(1)).id
17 | selectGame: (gameId) ->
18 | @setState(selectedGame: gameId)
19 | handleImport: (evt) ->
20 | file = evt.target.files[0]
21 | reader = new FileReader()
22 | reader.onload = (fEvt) =>
23 | args = JSON.parse(fEvt.target.result)
24 | @importGame(args)
25 | reader.readAsText file
26 | importGame: (game) ->
27 | @refs.gameSetup.reloadState()
28 | GameState.new(game)
29 | .then (gs) =>
30 | @setState(newGame: gs)
31 | onChange: () ->
32 | GameMetadata.all().then (games) =>
33 | @setState(games: games)
34 | openGame: () ->
35 | gameId = React.findDOMNode(@refs.gameSelect).value
36 | if gameId? and gameId.length > 0
37 | @selectGame(gameId)
38 | componentDidMount: () ->
39 | @onChange()
40 | GameMetadata.addChangeListener @onChange
41 | GameState.new().then (game) =>
42 | @setState(newGame: game)
43 | componentWillUnmount: () ->
44 | GameMetadata.removeChangeListener @onChange
45 | render: () ->
46 | hideIfSelected = cx
47 | 'hidden': @state.selectedGame?
48 | 'container': true
49 |
50 |
51 |
Game Select
52 |
53 |
54 |
55 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
Game Import
67 |
68 | {if @state.newGame?
69 |
70 | }
71 |
72 | {if @state.selectedGame?
73 |
74 | }
75 |
76 |
77 |
--------------------------------------------------------------------------------
/test/models/pass-test.coffee:
--------------------------------------------------------------------------------
1 | jest.mock '../../app/scripts/models/skater'
2 | constants = require '../../app/scripts/constants'
3 | ActionTypes = constants.ActionTypes
4 | MemoryStorage = require '../../app/scripts/memory_storage'
5 | describe 'Pass', () ->
6 | process.setMaxListeners(0)
7 | AppDispatcher = undefined
8 | Pass = undefined
9 | callback = undefined
10 | beforeEach () ->
11 | AppDispatcher = require '../../app/scripts/dispatcher/app_dispatcher'
12 | Pass = require '../../app/scripts/models/pass'
13 | Pass.store = new MemoryStorage()
14 | callback = AppDispatcher.register.mock.calls[0][0]
15 | it 'registers a callback with the dispatcher', () ->
16 | expect(AppDispatcher.register.mock.calls.length).toBe(1)
17 | pit 'initializes with no items', () ->
18 | Pass.all().then (passes) ->
19 | expect(passes.length).toBe(0)
20 | describe "actions", () ->
21 | pass = undefined
22 | beforeEach () ->
23 | pass = Pass.new().tap Pass.save
24 | pit "toggles injury", () ->
25 | pass.then (pass) ->
26 | callback
27 | type: ActionTypes.TOGGLE_INJURY
28 | passId: pass.id
29 | .then (pass) ->
30 | expect(pass.injury).toBe(true)
31 | pit "toggles no pass", () ->
32 | pass.then (pass) ->
33 | callback
34 | type: ActionTypes.TOGGLE_NOPASS
35 | passId: pass.id
36 | .then (pass) ->
37 | expect(pass.nopass).toBe(true)
38 | pit "toggles calloff", () ->
39 | pass.then (pass) ->
40 | callback
41 | type: ActionTypes.TOGGLE_CALLOFF
42 | passId: pass.id
43 | .then (pass) ->
44 | expect(pass.calloff).toBe(true)
45 | pit "toggles lead", () ->
46 | pass.then (pass) ->
47 | callback
48 | type: ActionTypes.TOGGLE_LEAD
49 | passId: pass.id
50 | .then (pass) ->
51 | expect(pass.lead).toBe(true)
52 | pit "toggles lost lead", () ->
53 | pass.then (pass) ->
54 | callback
55 | type: ActionTypes.TOGGLE_LOST_LEAD
56 | passId: pass.id
57 | .then (pass) ->
58 | expect(pass.lostLead).toBe(true)
59 | pit "sets points", () ->
60 | pass.then (pass) ->
61 | callback
62 | type: ActionTypes.SET_POINTS
63 | passId: pass.id
64 | points: 4
65 | .then (pass) ->
66 | expect(pass.points).toBe(4)
67 | pit "sets the pass jammer", () ->
68 | pass.then (pass) ->
69 | callback
70 | type: ActionTypes.SET_PASS_JAMMER
71 | passId: pass.id
72 | skaterId: 'skater 1'
73 | .then (pass) ->
74 | expect(pass.jammer.id).toBe('skater 1')
75 | pit "removes a pass", () ->
76 | pass.then (pass) ->
77 | callback
78 | type: ActionTypes.REMOVE_PASS
79 | passId: pass.id
80 | .then () ->
81 | Pass.all()
82 | .then (passes) ->
83 | expect(passes.length).toBe(0)
84 |
--------------------------------------------------------------------------------
/app/scripts/constants.coffee:
--------------------------------------------------------------------------------
1 | keyMirror = require 'keymirror'
2 | module.exports =
3 | HALFTIME_DURATION_IN_MS: 30*60*1000
4 | PREGAME_DURATION_IN_MS: 60*60*1000
5 | CLOCK_SYNC_SAMPLE_DURATION_IN_MS: 10*1000
6 | CLOCK_SYNC_SAMPLE_DURATION_MULTIPLIER: 1.15
7 | CLOCK_SYNC_DELAY_DURATION_IN_MS: 1*1000
8 | WEBSOCKETS_RETRY_TIME_IN_MS: 3000
9 | PERIOD_DURATION_IN_MS: 30*60*1000
10 | JAM_DURATION_IN_MS: 2*60*1000
11 | JAM_WARNING_IN_MS: 10*1000
12 | LINEUP_DURATION_IN_MS: 30*1000
13 | OVERTIME_DURATION_IN_MS: 60*1000
14 | TIMEOUT_DURATION_IN_MS: 60*1000
15 | PENALTY_DURATION_IN_MS: 30*1000
16 | PENALTY_WARNING_IN_MS: 10*1000
17 | CLOCK_REFRESH_RATE_IN_MS: 500
18 | AD_DISPLAY_TIME_IN_MS: 10*1000
19 | GAMES_STATES: ["pregame", "halftime", "jam", "lineup", "timeout", "unofficial_final", "final"]
20 | TIMEOUT_STATES: ["official_timeout", "home_team_timeout", "home_team_official_review", "away_team_timeout", "away_team_official_review"]
21 | HOUR_IN_MS: 3600000
22 | MINUTE_IN_MS: 60000
23 | SECOND_IN_MS: 1000
24 | ActionTypes: keyMirror
25 | START_CLOCK: null
26 | STOP_CLOCK: null
27 | START_JAM: null
28 | STOP_JAM: null
29 | START_LINEUP: null
30 | START_PREGAME: null
31 | START_HALFTIME: null
32 | START_UNOFFICIAL_FINAL: null
33 | START_OFFICIAL_FINAL: null
34 | START_OVERTIME: null
35 | START_TIMEOUT: null
36 | HANDLE_CLOCK_EXPIRATION: null
37 | SET_TIMEOUT_AS_OFFICIAL_TIMEOUT: null
38 | SET_TIMEOUT_AS_HOME_TEAM_TIMEOUT: null
39 | SET_TIMEOUT_AS_HOME_TEAM_OFFICIAL_REVIEW: null
40 | SET_TIMEOUT_AS_AWAY_TEAM_TIMEOUT: null
41 | SET_TIMEOUT_AS_AWAY_TEAM_OFFICIAL_REVIEW: null
42 | SET_JAM_ENDED_BY_TIME: null
43 | SET_JAM_ENDED_BY_CALLOFF: null
44 | SET_JAM_CLOCK: null
45 | SET_PERIOD_CLOCK: null
46 | SET_HOME_TEAM_TIMEOUTS: null
47 | SET_AWAY_TEAM_TIMEOUTS: null
48 | SET_PERIOD: null
49 | SET_JAM_NUMBER: null
50 | REMOVE_HOME_TEAM_OFFICIAL_REVIEW: null
51 | REMOVE_AWAY_TEAM_OFFICIAL_REVIEW: null
52 | RESTORE_HOME_TEAM_OFFICIAL_REVIEW: null
53 | RESTORE_AWAY_TEAM_OFFICIAL_REVIEW: null
54 | TOGGLE_NO_PIVOT: null
55 | TOGGLE_STAR_PASS: null
56 | SET_SKATER_POSITION: null
57 | CYCLE_LINEUP_STATUS: null
58 | CREATE_NEXT_JAM: null
59 | CREATE_NEXT_PASS: null
60 | TOGGLE_INJURY: null
61 | TOGGLE_NOPASS: null
62 | TOGGLE_CALLOFF: null
63 | TOGGLE_LOST_LEAD: null
64 | TOGGLE_LEAD: null
65 | SET_STAR_PASS: null
66 | SET_POINTS: null
67 | REORDER_PASS: null
68 | SET_PASS_JAMMER: null
69 | SET_PENALTY: null
70 | CLEAR_PENALTY: null
71 | UPDATE_PENALTY: null
72 | TOGGLE_LEFT_EARLY: null
73 | TOGGLE_PENALTY_SERVED: null
74 | SET_PENALTY_BOX_SKATER: null
75 | TOGGLE_PENALTY_TIMER: null
76 | TOGGLE_ALL_PENALTY_TIMERS: null
77 | SAVE_GAME: null
78 | SYNC_GAMES: null
79 | REMOVE_PASS: null
80 | REMOVE_JAM: null
81 | JAM_TIMER_UNDO: null
82 | JAM_TIMER_REDO: null
83 |
--------------------------------------------------------------------------------
/app/scripts/dispatcher/app_dispatcher.coffee:
--------------------------------------------------------------------------------
1 | invariant = require 'invariant'
2 | Promise = require 'bluebird'
3 | config = require '../config'
4 | constants = require '../constants'
5 | IO = require 'socket.io-client'
6 | class AppDispatcher
7 | constructor: () ->
8 | @_lastId = 1
9 | @_callbacks = {}
10 | @_promises = {}
11 | @_dispatch = Promise.resolve()
12 | @_timing = {}
13 | @_delays = []
14 | @_delay = 0
15 | @_socket = IO(config.socketUrl)
16 | @_socket.on 'app dispatcher', (payload) =>
17 | if payload.sourceDelay?
18 | payload.destinationDelay = @_delay
19 | @dispatch(payload)
20 | @_socket.on 'connected', () =>
21 | console.log "connected"
22 | @syncClocks()
23 | @_socket.on "clocks synced", (args) =>
24 | @clocksSynced(args)
25 | syncGame: (gameId) ->
26 | @_socket.emit 'sync game', gameId: gameId
27 | syncClocks: () ->
28 | @_timing.A = new Date().getTime()
29 | @_socket.emit 'sync clocks', {}
30 | clocksSynced: (args) =>
31 | @_timing.B = new Date().getTime()
32 | @_timing.X = args.timeX
33 | @_timing.Y = args.timeY
34 | @_delays.push(@_timing.B - @_timing.A - (@_timing.Y - @_timing.X))
35 | @_delay = @_delays.reduce((b,c)->
36 | return b+c
37 | )/@_delays.length
38 | console.log "Delay is #{@_delay}ms"
39 | setTimeout(() =>
40 | @syncClocks()
41 | ,constants.CLOCK_SYNC_SAMPLE_DURATION_IN_MS*constants.CLOCK_SYNC_SAMPLE_DURATION_MULTIPLIER**@_delays.length)
42 | register: (callback) ->
43 | id = @_lastId++
44 | @_callbacks[id] = callback
45 | id
46 | unregister: (id) ->
47 | invariant(
48 | @_callbacks[id],
49 | 'Dispatcher.unregister(...) `%s` does not map to a registered callback.',
50 | id)
51 | delete this_callbacks[id];
52 | waitFor: (ids) ->
53 | invariant(
54 | @isDispatching(),
55 | 'Dispatcher.waitFor(...): Must be invoked while dispatching.')
56 | Promise.map ids, (id) =>
57 | invariant(
58 | @_callbacks[id],
59 | 'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',
60 | id)
61 | @_promises[id]
62 | dispatch: (payload) ->
63 | console.log payload
64 | @_dispatch = @_dispatch.then =>
65 | @_promises = {}
66 | for id, callback of @_callbacks
67 | @_promises[id] = callback(payload)
68 | @_promises
69 | .props()
70 | isDispatching: () ->
71 | @_dispatch?.isPending()
72 | emit: (payload) ->
73 | @_socket.emit('app dispatcher', payload)
74 | dispatchAndEmit: (payload) ->
75 | @dispatch(payload)
76 | @emit(payload)
77 | isConnected: () ->
78 | @_socket.connected
79 | addConnectionListener: (listener) ->
80 | for evt in ['connect', 'disconnect', 'reconnect']
81 | @_socket.on evt, listener
82 | removeConnectionListener: (listenr) ->
83 | for evt in ['connect', 'disconnect', 'reconnect']
84 | @_socket.removeListener evt, listener
85 | module.exports = new AppDispatcher()
--------------------------------------------------------------------------------
/app/scripts/models/pass.coffee:
--------------------------------------------------------------------------------
1 | Promise = require 'bluebird'
2 | functions = require '../functions'
3 | AppDispatcher = require '../dispatcher/app_dispatcher'
4 | {ActionTypes} = require '../constants'
5 | Store = require './store'
6 | Skater = require './skater'
7 | class Pass extends Store
8 | @dispatchToken: AppDispatcher.register (action) =>
9 | switch action.type
10 | when ActionTypes.TOGGLE_INJURY
11 | @find(action.passId).then (pass) =>
12 | pass.toggleInjury()
13 | pass.save()
14 | when ActionTypes.TOGGLE_NOPASS
15 | @find(action.passId).then (pass) =>
16 | pass.toggleNopass()
17 | pass.save()
18 | when ActionTypes.TOGGLE_CALLOFF
19 | @find(action.passId).then (pass) =>
20 | pass.toggleCalloff()
21 | pass.save()
22 | when ActionTypes.TOGGLE_LOST_LEAD
23 | @find(action.passId).then (pass) =>
24 | pass.toggleLostLead()
25 | pass.save()
26 | when ActionTypes.TOGGLE_LEAD
27 | @find(action.passId).then (pass) =>
28 | pass.toggleLead()
29 | pass.save()
30 | when ActionTypes.SET_STAR_PASS
31 | @find(action.passId).then (pass) =>
32 | pass.setPoints(0)
33 | pass.save()
34 | when ActionTypes.SET_POINTS
35 | @find(action.passId).then (pass) =>
36 | pass.setPoints(action.points)
37 | pass.save()
38 | when ActionTypes.SET_PASS_JAMMER
39 | @find(action.passId).tap (pass) ->
40 | pass.setJammer(action.skaterId)
41 | .then (pass) ->
42 | pass.save()
43 | when ActionTypes.REMOVE_PASS
44 | @find(action.passId).then (pass) =>
45 | pass.destroy()
46 | constructor: (options={}) ->
47 | super options
48 | @jamId = options.jamId
49 | @passNumber = options.passNumber ? 1
50 | @points = options.points ? 0
51 | @jammer = new Skater(options.jammer) if options.jammer?
52 | @injury = options.injury ? false
53 | @lead = options.lead ? false
54 | @lostLead = options.lostLead ? false
55 | @calloff = options.calloff ? false
56 | @nopass = options.nopass ? false
57 | load: () ->
58 | if @jammer?
59 | Skater.new(@jammer).then (jammer) =>
60 | @jammer = jammer
61 | .return(this)
62 | else
63 | Promise.resolve(this)
64 | toggleInjury: () ->
65 | @injury = not @injury
66 | toggleNopass: () ->
67 | @nopass = not @nopass
68 | toggleCalloff: () ->
69 | @calloff = not @calloff
70 | toggleLostLead: () ->
71 | @lostLead = not @lostLead
72 | toggleLead: () ->
73 | @lead = not @lead
74 | setPoints: (points) ->
75 | @points = points
76 | setJammer: (skaterId) ->
77 | Skater.find(skaterId).then (skater) =>
78 | @jammer = skater
79 | getNotes: () ->
80 | flags =
81 | injury: @injury
82 | nopass: @nopass
83 | calloff: @calloff
84 | lost: @lostLead
85 | lead: @lead
86 | Object.keys(flags).filter (key) ->
87 | flags[key]
88 | module.exports = Pass
89 |
--------------------------------------------------------------------------------
/app/scripts/components/announcers_feed/feed_item.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | tinycolor = require 'tinycolor2'
3 | module.exports = React.createClass
4 | displayName: 'FeedItem'
5 | propTypes:
6 | item: React.PropTypes.object.isRequired
7 | renderTwoColumn: (icon, body, style) ->
8 |
9 |
10 |
11 | {icon}
12 |
13 |
14 |
15 |
16 | {body}
17 |
18 |
19 |
20 | render: () ->
21 | notLeadStyle = () =>
22 | style = @props.item.style
23 | backgroundColor: style.backgroundColor
24 | borderColor: style.borderColor
25 | color: tinycolor(style.backgroundColor).lighten(20).toString()
26 | hasLead = () =>
27 | passes = @props.item.jam.passes
28 | lead = passes.some (pass) ->
29 | pass.lead
30 | lost = passes.some (pass) ->
31 | pass.lostLead
32 | lead and not lost
33 | switch @props.item.type
34 | when 'jam start'
35 | Jam {@props.item.jamNumber} Starts
36 | when 'timeout'
37 | {@props.item.body}
38 | when 'penalty'
39 | @renderTwoColumn(
40 | @props.item.penalty.code,
41 | {@props.item.skater?.number} {@props.item.skater?.name} - Penalty: {@props.item.penalty.name},
42 | @props.item.style
43 | )
44 | when 'lead'
45 | @renderTwoColumn(
46 | ,
47 | {@props.item.skater?.number} {@props.item.skater?.name} - Lead Jammer,
48 | @props.item.style
49 | )
50 | when 'lost lead'
51 | @renderTwoColumn(
52 | ,
53 | {@props.item.skater?.number} {@props.item.skater?.name} - Lost Lead,
54 | notLeadStyle()
55 | )
56 | when 'calloff'
57 | @renderTwoColumn(
58 | ,
59 | {@props.item.skater?.number} {@props.item.skater?.name} - Calls the Jam,
60 | @props.item.style
61 | )
62 | when 'points'
63 | @renderTwoColumn(
64 | ,
65 | {@props.item.pass.jammer?.number} {@props.item.pass.jammer?.name} - {@props.item.pass.points} Points (Pass {@props.item.pass.passNumber}),
66 | if hasLead() then @props.item.style else notLeadStyle()
67 | )
--------------------------------------------------------------------------------
/test/models/team-test.coffee:
--------------------------------------------------------------------------------
1 | jest.mock '../../app/scripts/models/jam'
2 | jest.mock '../../app/scripts/models/skater'
3 | jest.mock '../../app/scripts/models/box_entry'
4 | _ = require 'underscore'
5 | constants = require '../../app/scripts/constants'
6 | ActionTypes = constants.ActionTypes
7 | MemoryStorage = require '../../app/scripts/memory_storage'
8 | {ClockManager, Clock} = require '../../app/scripts/clock'
9 | Skater = require '../../app/scripts/models/skater'
10 | describe 'Team', () ->
11 | process.setMaxListeners(0)
12 | AppDispatcher = undefined
13 | Team = undefined
14 | Jam = undefined
15 | callback = undefined
16 | beforeEach () ->
17 | AppDispatcher = require '../../app/scripts/dispatcher/app_dispatcher'
18 | Jam = require '../../app/scripts/models/jam'
19 | Team = require '../../app/scripts/models/team'
20 | callback = AppDispatcher.register.mock.calls[0][0]
21 | it 'registers a callback with the dispatcher', () ->
22 | expect(AppDispatcher.register.mock.calls.length).toBe(1)
23 | pit 'initializes with no items', () ->
24 | Team.all().then (teams) ->
25 | expect(teams.length).toBe(0)
26 | describe "actions", () ->
27 | team = undefined
28 | beforeEach () ->
29 | team = Team.new().tap Team.save
30 | pit "intializes with a single jam", () ->
31 | team.then (team) ->
32 | expect(team.jams.length).toBe(1)
33 | pit "creates the next jam", () ->
34 | team.then (team) ->
35 | callback
36 | type: ActionTypes.CREATE_NEXT_JAM
37 | teamId: team.id
38 | jamNumber: 2
39 | .then (team) ->
40 | expect(team.jams.length).toBe(2)
41 | pit "does not create duplicate jams", () ->
42 | team.then (team) ->
43 | callback
44 | type: ActionTypes.CREATE_NEXT_JAM
45 | teamId: team.id
46 | jamNumber: 2
47 | team.then (team) ->
48 | callback
49 | type: ActionTypes.CREATE_NEXT_JAM
50 | teamId: team.id
51 | jamNumber: 2
52 | .then (team) ->
53 | expect(team.jams.length).toBe(2)
54 | pit "renumbers jams after one is removed", () ->
55 | team.then (team) ->
56 | Jam.dispatchToken = teamId: team.id
57 | callback
58 | type: ActionTypes.REMOVE_JAM
59 | .then (team) ->
60 | expect(AppDispatcher.waitFor).toBeCalled()
61 | expect(team.jams[0].jamNumber).toBe(1)
62 | expect(team.jams[0].save).toBeCalled()
63 | pit "toggles all penalty timers", () ->
64 | team.then (team) ->
65 | callback
66 | type: ActionTypes.TOGGLE_ALL_PENALTY_TIMERS
67 | teamId: team.id
68 | .then (team) ->
69 | for seat in team.seats
70 | expect(seat.startPenaltyTimer).not.toBeCalled()
71 | seat.dirty = true
72 | seat.startPenaltyTimer.mockClear()
73 | team.toggleAllPenaltyTimers()
74 | for seat in team.seats
75 | expect(seat.startPenaltyTimer).toBeCalled()
76 | team.seats[0].penaltyTimerIsRunning.mockReturnValueOnce true
77 | team.toggleAllPenaltyTimers()
78 | for seat in team.seats
79 | expect(seat.stopPenaltyTimer).toBeCalled()
80 |
81 |
--------------------------------------------------------------------------------
/app/scripts/components/penalty_tracker/edit_penalty_panel.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | $ = require 'jquery'
3 | AppDispatcher = require '../../dispatcher/app_dispatcher.coffee'
4 | {ActionTypes} = require '../../constants.coffee'
5 | cx = React.addons.classSet
6 | module.exports = React.createClass
7 | displayName: 'EditPenaltyPanel'
8 | propTypes:
9 | penaltyNumber: React.PropTypes.number
10 | skaterPenalty: React.PropTypes.object.isRequired
11 | incrementJamNumber: React.PropTypes.func.isRequired
12 | decrementJamNumber: React.PropTypes.func.isRequired
13 | clearPenalty: React.PropTypes.func.isRequired
14 | toggleSat: React.PropTypes.func.isRequired
15 | toggleSeverity: React.PropTypes.func.isRequired
16 | onOpen: React.PropTypes.func.isRequired
17 | onClose: React.PropTypes.func.isRequired
18 | clearPenalty: () ->
19 | @closePanel()
20 | @props.clearPenalty()
21 | componentDidMount: () ->
22 | $dom = $(@getDOMNode())
23 | $dom.on 'show.bs.collapse', (evt) =>
24 | $('.edit-penalty.collapse.in').collapse('hide')
25 | $dom.on 'shown.bs.collapse', (evt) =>
26 | @props.onOpen()
27 | $dom.on 'hide.bs.collapse', (evt) =>
28 | @props.onClose()
29 | render: () ->
30 | classArgs =
31 | 'edit-penalty collapse': true
32 | classArgs["penalty-#{@props.penaltyNumber % 7}"] = true
33 | containerClass = cx classArgs
34 | satClass = cx
35 | 'bt-btn': true
36 | 'btn-primary': @props.skaterPenalty.sat
37 | severityClass = cx
38 | 'bt-btn': true
39 | 'btn-primary': @props.skaterPenalty.penalty.egregious
40 |
41 |
42 |
43 |
46 |
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 | Jam {@props.skaterPenalty.jamNumber}
57 |
58 |
59 |
62 |
63 |
64 |
65 |
66 |
67 |
70 |
71 |
72 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/demo.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Demo Game",
3 | "venue": "The Internet",
4 | "date": "07\/31\/2015",
5 | "time": "5:00 PM",
6 | "home": {
7 | "name": "Atlanta",
8 | "initials": "ARG",
9 | "colorBarStyle": {
10 | "backgroundColor": "#2082a6",
11 | "color": "#ffffff"
12 | },
13 | "logo": "\/images\/team_logos\/Atlanta.png",
14 | "skaters": [
15 | {
16 | "name": "Wild Cherri",
17 | "number": "6"
18 | },
19 | {
20 | "name": "Rebel Yellow",
21 | "number": "12AM"
22 | },
23 | {
24 | "name": "Agent Maulder",
25 | "number": "X13"
26 | },
27 | {
28 | "name": "Alassin Sane",
29 | "number": "1973"
30 | },
31 | {
32 | "name": "Amelia Scareheart",
33 | "number": "B52"
34 | },
35 | {
36 | "name": "Belle of the Brawl",
37 | "number": "32"
38 | },
39 | {
40 | "name": "Bruze Orman",
41 | "number": "850"
42 | },
43 | {
44 | "name": "ChokeCherry",
45 | "number": "86"
46 | },
47 | {
48 | "name": "Hollicidal",
49 | "number": "1013"
50 | },
51 | {
52 | "name": "Jammunition",
53 | "number": "50CAL"
54 | },
55 | {
56 | "name": "Jean-Juke Picard",
57 | "number": "1701"
58 | },
59 | {
60 | "name": "Madditude Adjustment",
61 | "number": "23"
62 | },
63 | {
64 | "name": "Nattie Long Legs",
65 | "number": "504"
66 | },
67 | {
68 | "name": "Ozzie Kamakazi",
69 | "number": "747"
70 | }
71 | ]
72 | },
73 | "away": {
74 | "name": "Gotham",
75 | "initials": "GGRD",
76 | "colorBarStyle": {
77 | "backgroundColor": "#f50031",
78 | "color": "#ffffff"
79 | },
80 | "logo": "\/images\/team_logos\/Gotham.png",
81 | "skaters": [
82 | {
83 | "name": "Ana Bollocks",
84 | "number": "00"
85 | },
86 | {
87 | "name": "Bonita Apple Bomb",
88 | "number": "4500\u00ba"
89 | },
90 | {
91 | "name": "Bonnie Thunders",
92 | "number": "340"
93 | },
94 | {
95 | "name": "Caf Fiend",
96 | "number": "314"
97 | },
98 | {
99 | "name": "Claire D. Way",
100 | "number": "1984"
101 | },
102 | {
103 | "name": "Davey Blockit",
104 | "number": "929"
105 | },
106 | {
107 | "name": "Donna Matrix",
108 | "number": "2"
109 | },
110 | {
111 | "name": "Fast and Luce",
112 | "number": "17"
113 | },
114 | {
115 | "name": "Fisti Cuffs",
116 | "number": "241"
117 | },
118 | {
119 | "name": "Hyper Lynx",
120 | "number": "404"
121 | },
122 | {
123 | "name": "Mick Swagger",
124 | "number": "53"
125 | },
126 | {
127 | "name": "Miss Tea Maven",
128 | "number": "1706"
129 | },
130 | {
131 | "name": "OMG WTF",
132 | "number": "753"
133 | },
134 | {
135 | "name": "Puss 'n Glues",
136 | "number": "999 Lives"
137 | }
138 | ]
139 | }
140 | }
--------------------------------------------------------------------------------
/app/scripts/components/penalty_box_timer/penalty_clock.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | functions = require '../../functions.coffee'
3 | {ClockManager} = require '../../clock.coffee'
4 | SkaterSelector = require '../shared/skater_selector.cjsx'
5 | AppDispatcher = require '../../dispatcher/app_dispatcher.coffee'
6 | {ActionTypes} = require '../../constants.coffee'
7 | cx = React.addons.classSet
8 | module.exports = React.createClass
9 | displayName: "PenaltyClock"
10 | propTypes:
11 | team: React.PropTypes.object.isRequired
12 | setSelectorContext: React.PropTypes.func.isRequired
13 | entry: React.PropTypes.object.isRequired
14 | componentWillMount: () ->
15 | @clockManager = new ClockManager()
16 | componentDidMount: () ->
17 | @clockManager.addTickListener @onTick
18 | componentWillUnmount: () ->
19 | @clockManager.removeTickListener @onTick
20 | onTick: () ->
21 | @forceUpdate()
22 | toggleLeftEarly: () ->
23 | AppDispatcher.dispatchAndEmit
24 | type: ActionTypes.TOGGLE_LEFT_EARLY
25 | boxId: @props.entry.id
26 | toggleServed: () ->
27 | AppDispatcher.dispatchAndEmit
28 | type: ActionTypes.TOGGLE_PENALTY_SERVED
29 | boxId: @props.entry.id
30 | setSkater: (skaterId) ->
31 | AppDispatcher.dispatchAndEmit
32 | type: ActionTypes.SET_PENALTY_BOX_SKATER
33 | boxId: @props.entry.id
34 | skaterId: skaterId
35 | togglePenaltyTimer: () ->
36 | AppDispatcher.dispatchAndEmit
37 | type: ActionTypes.TOGGLE_PENALTY_TIMER
38 | boxId: @props.entry.id
39 | render: () ->
40 | teamStyle = @props.team.colorBarStyle
41 | placeholder = switch @props.entry.position
42 | when 'jammer' then "Jammer"
43 | when 'blocker' then "Blocker"
44 | containerClass = cx
45 | 'penalty-clock': true
46 | 'hidden': @props.hidden
47 | leftEarlyButtonClass = cx
48 | 'bt-btn': true
49 | 'btn-warning': @props.entry.leftEarly
50 | servedButtonClass = cx
51 | 'bt-btn': true
52 | 'btn-success': @props.entry.served
53 |
54 |
55 |
56 |
67 |
68 |
69 |
72 |
73 |
74 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/app/scripts/components/game_setup/team_fields.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | $ = require 'jquery'
3 | require 'jquery-minicolors'
4 | RosterFields = require './roster_fields'
5 | cx = React.addons.classSet
6 | module.exports = React.createClass
7 | displayName: 'TeamFields'
8 | componentDidMount: () ->
9 | $dom = $(@getDOMNode())
10 | $dom.find(".background-color.colorpicker").minicolors
11 | theme: 'bootstrap'
12 | changeDelay: 200
13 | change: (color) =>
14 | @props.actions.updateTeam @props.teamState,
15 | colorBarStyle:
16 | backgroundColor: color
17 | $dom.find(".text-color.colorpicker").minicolors
18 | theme: 'bootstrap'
19 | changeDelay: 200
20 | change: (color) =>
21 | @props.actions.updateTeam @props.teamState,
22 | colorBarStyle:
23 | color: color
24 | handleNameChange: (evt) ->
25 | @props.actions.updateTeam @props.teamState, name: evt.target.value
26 | handleInitialsChange: (evt) ->
27 | @props.actions.updateTeam @props.teamState, initials: evt.target.value
28 | handleBackgroundColorChange: (evt) ->
29 | @props.actions.updateTeam @props.teamState,
30 | colorBarStyle:
31 | backgroundColor: evt.target.value
32 | handleTextColorChange: (evt) ->
33 | @props.actions.updateTeam @props.teamState,
34 | colorBarStyle:
35 | color: evt.target.value
36 | handleLogoChange: (evt) ->
37 | file = evt.target.files[0]
38 | reader = new FileReader()
39 | reader.onload = (fEvt) =>
40 | @props.actions.updateTeam @props.teamState,
41 | logo: fEvt.target.result
42 | reader.readAsDataURL file
43 | render: () ->
44 |
45 |
{@props.teamType} Team
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |

66 |
67 |
68 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the WFTDA Board of Directors (https://wftda.com/board-of-directors).
59 | All complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
--------------------------------------------------------------------------------
/app/scripts/demo_data.coffee:
--------------------------------------------------------------------------------
1 | module.exports =
2 | init: () ->
3 | GameState = require './models/game_state'
4 | Team = require './models/team'
5 | Skater = require './models/skater'
6 | homeSkaters = [
7 | {name: "Wild Cherri"
8 | number: "6"
9 | penalties: []}
10 | ,
11 | {name: "Rebel Yellow"
12 | number: "12AM"
13 | penalties: []}
14 | ,
15 | {name: "Agent Maulder"
16 | number: "X13"
17 | penalties: []}
18 | ,
19 | {name: "Alassin Sane"
20 | number: "1973"
21 | penalties: []}
22 | ,
23 | {name: "Amelia Scareheart"
24 | number: "B52"
25 | penalties: []}
26 | ,
27 | {name: "Belle of the Brawl"
28 | number: "32"
29 | penalties: []}
30 | ,
31 | {name: "Bruze Orman"
32 | number: "850"
33 | penalties: []}
34 | ,
35 | {name: "ChokeCherry"
36 | number: "86"
37 | penalties: []}
38 | ,
39 | {name: "Hollicidal"
40 | number: "1013"
41 | penalties: []}
42 | ,
43 | {name: "Jammunition"
44 | number: "50CAL"
45 | penalties: []}
46 | ,
47 | {name: "Jean-Juke Picard"
48 | number: "1701"
49 | penalties: []}
50 | ,
51 | {name: "Madditude Adjustment"
52 | number: "23"
53 | penalties: []}
54 | ,
55 | {name: "Nattie Long Legs",
56 | number: "504"
57 | penalties: []}
58 | ,
59 | {name: "Ozzie Kamakazi"
60 | number: "747"
61 | penalties: []}
62 | ]
63 | awaySkaters = [
64 | {name: "Ana Bollocks"
65 | number: "00"
66 | penalties: []}
67 | ,
68 | {name: "Bonita Apple Bomb"
69 | number: "4500º"
70 | penalties: []}
71 | ,
72 | {name: "Bonnie Thunders"
73 | number: "340"
74 | penalties: []}
75 | ,
76 | {name: "Caf Fiend"
77 | number: "314"
78 | penalties: []}
79 | ,
80 | {name: "Claire D. Way"
81 | number: "1984"
82 | penalties: []}
83 | ,
84 | {name: "Davey Blockit"
85 | number: "929"
86 | penalties: []}
87 | ,
88 | {name: "Donna Matrix",
89 | number: "2"
90 | penalties: []}
91 | ,
92 | {name: "Fast and Luce"
93 | number: "17"
94 | penalties: []}
95 | ,
96 | {name: "Fisti Cuffs"
97 | number: "241"
98 | penalties: []}
99 | ,
100 | {name: "Hyper Lynx"
101 | number: "404"
102 | penalties: []}
103 | ,
104 | {name: "Mick Swagger"
105 | number: "53"
106 | penalties: []}
107 | ,
108 | {name: "Miss Tea Maven"
109 | number: "1706"
110 | penalties: []}
111 | ,
112 | {name: "OMG WTF"
113 | number: "753"
114 | penalties: []}
115 | ,
116 | {name: "Puss 'n Glues"
117 | number: "999 Lives"
118 | penalties: []}
119 | ]
120 | GameState.new
121 | name: "Demo Game"
122 | venue: "The Internet"
123 | date: "07/31/2015"
124 | time: "5:00 PM"
125 | home:
126 | name: "Atlanta"
127 | initials: "ARG"
128 | colorBarStyle:
129 | backgroundColor: "#2082a6"
130 | color: "#ffffff"
131 | logo: "/images/team_logos/Atlanta.png"
132 | skaters: homeSkaters
133 | away:
134 | name: "Gotham"
135 | initials: "GGRD"
136 | colorBarStyle:
137 | backgroundColor: "#f50031"
138 | color: "#ffffff"
139 | logo: "/images/team_logos/Gotham.png"
140 | skaters: awaySkaters
--------------------------------------------------------------------------------
/app/scripts/components/navbar.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | cx = React.addons.classSet
3 | module.exports = React.createClass
4 | render: () ->
5 | jamTimerCS = cx
6 | 'active': @props.tab == "jam_timer"
7 | lineupTrackerCS = cx
8 | 'active': @props.tab == "lineup_tracker"
9 | scorekeeperCS = cx
10 | 'active': @props.tab == "scorekeeper"
11 | penaltyTrackerCS = cx
12 | 'active': @props.tab == "penalty_tracker"
13 | penaltyBoxTimerCS = cx
14 | 'active': @props.tab == "penalty_box_timer"
15 | scoreboardCS = cx
16 | 'active': @props.tab == "scoreboard"
17 | penaltyWhiteboardCS = cx
18 | 'active': @props.tab == "penalty_whiteboard"
19 | announcersFeedCS = cx
20 | 'active': @props.tab == "announcers_feed"
21 |
79 |
--------------------------------------------------------------------------------
/app/scripts/components/lineup_tracker/jam_detail.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | AppDispatcher = require '../../dispatcher/app_dispatcher.coffee'
3 | {ActionTypes} = require '../../constants.coffee'
4 | SkaterSelector = require '../shared/skater_selector'
5 | ItemRow = require '../shared/item_row'
6 | LineupBoxRow = require './lineup_box_row'
7 | cx = React.addons.classSet
8 | module.exports = React.createClass
9 | displayName: 'JamDetail'
10 | propTypes:
11 | team: React.PropTypes.object.isRequired
12 | jam: React.PropTypes.object.isRequired
13 | setSelectorContextHandler: React.PropTypes.func.isRequired
14 | toggleNoPivot: () ->
15 | AppDispatcher.dispatchAndEmit
16 | type: ActionTypes.TOGGLE_NO_PIVOT
17 | jamId: @props.jam.id
18 | toggleStarPass: () ->
19 | AppDispatcher.dispatchAndEmit
20 | type: ActionTypes.TOGGLE_STAR_PASS
21 | jamId: @props.jam.id
22 | setSkaterPosition: (position, skaterId) ->
23 | AppDispatcher.dispatchAndEmit
24 | type: ActionTypes.SET_SKATER_POSITION
25 | jamId: @props.jam.id
26 | position: position
27 | skaterId: skaterId
28 | cycleLineupStatus: (statusIndex, position) ->
29 | AppDispatcher.dispatchAndEmit
30 | type: ActionTypes.CYCLE_LINEUP_STATUS
31 | jamId: @props.jam.id
32 | statusIndex: statusIndex
33 | position: position
34 | removeJam: () ->
35 | AppDispatcher.dispatchAndEmit
36 | type: ActionTypes.REMOVE_JAM
37 | jamId: @props.jam.id
38 | renderItem: () ->
39 | noPivotButtonClass = cx
40 | 'bt-btn': true
41 | 'btn-selected': @props.jam.noPivot
42 | starPassButtonClass = cx
43 | 'bt-btn': true
44 | 'btn-selected': @props.jam.starPass
45 |
46 |
47 |
48 | Jam {@props.jam.jamNumber}
49 |
50 |
51 |
52 |
55 |
56 |
57 |
60 |
61 |
62 | renderPanel: () ->
63 | actionsClass = cx
64 | 'row': true
65 | 'gutters-xs': true
66 | 'actions': true
67 |
68 |
69 | {@props.jam.listPositionLabels().map (pos) ->
70 |
71 | {pos}
72 |
73 | , this}
74 |
75 |
76 | {@props.jam.listPositions().map (pos) ->
77 |
78 |
85 |
86 | , this}
87 |
88 | {@props.jam.lineupStatuses.map (lineupStatus, statusIndex) ->
89 |
94 | , this }
95 |
99 |
100 | render: () ->
101 |
102 |
106 |
107 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bouttime/bouttime",
3 | "version": "0.0.30",
4 | "author": "Derby BoutTime",
5 | "description": "BoutTime App",
6 | "license": "Apache 2.0",
7 | "main": "dist/server.js",
8 | "bin": {
9 | "bouttime-server": "bin/bouttime-server"
10 | },
11 | "scripts": {
12 | "build": "bower install && gulp",
13 | "start": "gulp && bin/bouttime-server",
14 | "test": "jest",
15 | "watch": "bower install && gulp watch"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/DerbyBoutTime/bouttime.git"
20 | },
21 | "bugs": {
22 | "url": "https://github.com/DerbyBoutTime/bouttime/issues"
23 | },
24 | "homepage": "https://github.com/DerbyBoutTime/bouttime",
25 | "dependencies": {
26 | "bluebird": "2.9.30",
27 | "commander": "2.9.0",
28 | "express": "4.13.3",
29 | "invariant": "2.1.2",
30 | "jquery": "2.1.4",
31 | "keymirror": "0.1.1",
32 | "moment": "2.10.6",
33 | "moment-duration-format": "1.3.0",
34 | "mousetrap": "1.5.3",
35 | "nedb": "1.2.1",
36 | "querystring": "0.2.0",
37 | "react": "0.13.3",
38 | "seedrandom": "2.4.2",
39 | "socket.io": "1.3.7",
40 | "socket.io-client": "1.3.5",
41 | "tinycolor2": "1.1.2",
42 | "underscore": "1.8.3"
43 | },
44 | "devDependencies": {
45 | "bower": "1.7.9",
46 | "browserify": "9.0.8",
47 | "browserify-shim": "3.8.11",
48 | "coffee-react-transform": "3.1.0",
49 | "coffee-reactify": "3.0.0",
50 | "coffee-script": "1.10.0",
51 | "coffeeify": "1.1.0",
52 | "del": "1.2.1",
53 | "eslint": "1.10.3",
54 | "gulp": "3.9.0",
55 | "gulp-autoprefixer": "2.3.1",
56 | "gulp-bower": "0.0.10",
57 | "gulp-cache": "0.2.10",
58 | "gulp-cjsx": "3.0.0",
59 | "gulp-coffee": "2.3.1",
60 | "gulp-jshint": "1.12.0",
61 | "gulp-load-plugins": "0.10.0",
62 | "gulp-sass": "1.3.3",
63 | "gulp-size": "1.3.0",
64 | "gulp-sourcemaps": "1.6.0",
65 | "gulp-strip-debug": "1.0.2",
66 | "gulp-uglify": "1.4.2",
67 | "gulp-useref": "1.3.0",
68 | "gulp-util": "3.0.7",
69 | "gulp-webserver": "0.9.1",
70 | "jest-cli": "0.4.19",
71 | "main-bower-files": "2.9.0",
72 | "react-tools": "0.13.1",
73 | "require-dir": "0.3.0",
74 | "strip-debug": "1.1.1",
75 | "test": "0.6.0",
76 | "vinyl-source-stream": "1.1.0",
77 | "watchify": "3.6.0"
78 | },
79 | "browser": {
80 | "jquery-minicolors": "./app/bower_components/jquery-minicolors/jquery.minicolors.min.js",
81 | "bootstrap": "./app/bower_components/bootstrap-sass/assets/javascripts/bootstrap.min.js",
82 | "bootstrap-datetimepicker": "./app/bower_components/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js"
83 | },
84 | "browserify": {
85 | "transform": [
86 | "browserify-shim",
87 | "coffee-reactify"
88 | ]
89 | },
90 | "browserify-shim": {
91 | "jquery-minicolors": {
92 | "depends": [
93 | "jquery:$"
94 | ]
95 | },
96 | "bootstrap": {
97 | "depends": [
98 | "jquery:jQuery"
99 | ]
100 | },
101 | "bootstrap-datetimepicker": {
102 | "depends": [
103 | "jquery:$",
104 | "moment:moment"
105 | ]
106 | }
107 | },
108 | "jest": {
109 | "testDirectoryName": "test",
110 | "scriptPreprocessor": "preprocessor.js",
111 | "testFileExtensions": [
112 | "coffee",
113 | "litcoffee",
114 | "coffee.md",
115 | "cjsx",
116 | "js"
117 | ],
118 | "moduleFileExtensions": [
119 | "coffee",
120 | "litcoffee",
121 | "coffee.md",
122 | "cjsx",
123 | "js"
124 | ],
125 | "unmockedModulePathPatterns": [
126 | "bluebird",
127 | "nedb",
128 | "react",
129 | "react/addons",
130 | "jquery",
131 | "underscore",
132 | "moment",
133 | "moment-duration-format",
134 | "keymirror",
135 | "socket.io-client",
136 | "seedrandom",
137 | "app/scripts/components",
138 | "app/scripts/models",
139 | "app/scripts/demo_data",
140 | "app/scripts/constants",
141 | "app/scripts/functions",
142 | "app/scripts/memory_storage"
143 | ]
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/app/scripts/components/scorekeeper/jams_list.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | $ = require 'jquery'
3 | AppDispatcher = require '../../dispatcher/app_dispatcher.coffee'
4 | {ActionTypes} = require '../../constants.coffee'
5 | functions = require '../../functions.coffee'
6 | ItemRow = require '../shared/item_row'
7 | JamItem = require './jam_item'
8 | JamDetails = require './jam_details'
9 | Jam = require '../../models/jam.coffee'
10 | cx = React.addons.classSet
11 | module.exports = React.createClass
12 | displayName: 'JamsList'
13 | propTypes:
14 | team: React.PropTypes.object.isRequired
15 | setSelectorContext: React.PropTypes.func.isRequired
16 | selectedJam: () ->
17 | @props.team.jams[@state.jamSelected ? 0]
18 | handleMainMenu: () ->
19 | @setState(jamSelected: null)
20 | handleJamSelection: (jamIndex) ->
21 | @setState(jamSelected: jamIndex)
22 | handleNextJam: () ->
23 | if @state.jamSelected < @props.team.jams.length - 1
24 | $('.scorekeeper .collapse.in').collapse('hide')
25 | @setState(jamSelected: @state.jamSelected + 1)
26 | handlePreviousJam: () ->
27 | if @state.jamSelected > 0
28 | $('.scorekeeper .collapse.in').collapse('hide')
29 | @setState(jamSelected: @state.jamSelected - 1)
30 | createNextJam: () ->
31 | AppDispatcher.dispatchAndEmit
32 | type: ActionTypes.CREATE_NEXT_JAM
33 | teamId: @props.team.id
34 | jamNumber: @props.team.jams.length + 1
35 | removeJam: (jamId) ->
36 | AppDispatcher.dispatchAndEmit
37 | type: ActionTypes.REMOVE_JAM
38 | jamId: jamId
39 | getInitialState: () ->
40 | jamSelected: null
41 | render: () ->
42 | jamsContainerClass = cx
43 | 'jams fade-hide': true
44 | 'in': !@state.jamSelected?
45 | passesContainerClass = cx
46 | 'passes-container fade-hide': true
47 | 'in': @state.jamSelected?
48 |
49 |
50 |
51 |
52 |
53 |
54 | Current Jam
55 |
56 |
57 | {@props.jamNumber}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Game Total
67 |
68 |
69 | {@props.team.getPoints()}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | Jam
79 |
80 |
81 | Skater
82 |
83 |
84 | Notes
85 |
86 |
87 | Points
88 |
89 |
90 | {@props.team.jams.map (jam, jamIndex) ->
91 | item =
96 |
100 | , this}
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
114 |
115 |
--------------------------------------------------------------------------------
/app/scripts/components/game_setup/game_form.cjsx:
--------------------------------------------------------------------------------
1 | React = require 'react/addons'
2 | $ = require 'jquery'
3 | require 'bootstrap-datetimepicker'
4 | moment = require 'moment'
5 | TeamFields = require './team_fields'
6 | cx = React.addons.classSet
7 | module.exports = React.createClass
8 | displayName: 'GameForm'
9 | componentDidMount: () ->
10 | $dom = $(@getDOMNode())
11 | $dom.find('.datepicker').datetimepicker
12 | format: 'MM/DD/YYYY'
13 | .on 'dp.change', (evt) =>
14 | @props.actions.updateGame date: moment(evt.date).format('MM/DD/YYYY')
15 | $dom.find('.timepicker').datetimepicker
16 | format: 'LT'
17 | .on 'dp.change', (evt) =>
18 | @props.actions.updateGame time: moment(evt.date).format('LT')
19 | handleNameChange: (evt) ->
20 | @props.actions.updateGame name: evt.target.value
21 | handleVenueChange: (evt) ->
22 | @props.actions.updateGame venue: evt.target.value
23 | handleDateChange: (evt) ->
24 | @props.actions.updateGame date: evt.target.value
25 | handleTimeChange: (evt) ->
26 | @props.actions.updateGame time: evt.target.value
27 | handleOfficialChange: (idx, evt) ->
28 | @props.actions.updateOfficial idx, evt.target.value
29 | handleAdChange: (evt) ->
30 | file = evt.target.files[0]
31 | reader = new FileReader()
32 | reader.onload = (fEvt) =>
33 | @props.actions.addAd @props.gameState, fEvt.target.result
34 | reader.readAsDataURL file
35 | handleSubmit: (evt) ->
36 | evt.preventDefault()
37 | @props.actions.saveGame()
38 | render: () ->
39 |
95 |
--------------------------------------------------------------------------------
/test/models/jam-test.coffee:
--------------------------------------------------------------------------------
1 | jest.mock('../../app/scripts/models/pass')
2 | jest.mock('../../app/scripts/models/skater')
3 | constants = require '../../app/scripts/constants'
4 | ActionTypes = constants.ActionTypes
5 | Promise = require 'bluebird'
6 | describe 'Jam', () ->
7 | process.setMaxListeners(0)
8 | AppDispatcher = undefined
9 | Pass = undefined
10 | Jam = undefined
11 | Skater = undefined
12 | callback = undefined
13 | beforeEach () ->
14 | AppDispatcher = require '../../app/scripts/dispatcher/app_dispatcher'
15 | Pass = require '../../app/scripts/models/pass'
16 | Skater = require '../../app/scripts/models/skater'
17 | Jam = require '../../app/scripts/models/jam'
18 | callback = AppDispatcher.register.mock.calls[0][0]
19 | it 'registers a callback with the dispatcher', () ->
20 | expect(AppDispatcher.register.mock.calls.length).toBe(1)
21 | pit 'initializes with no items', () ->
22 | Jam.all().then (jams) ->
23 | expect(jams.length).toBe(0)
24 | describe "actions", () ->
25 | jam = undefined
26 | beforeEach () ->
27 | jam = Jam.new().tap Jam.save
28 | pit "saves a new jam", () ->
29 | jam.then (jam) ->
30 | Jam.all()
31 | .then (jams) ->
32 | expect(jams.length).toBe(1)
33 | pit "toggles no pivot", () ->
34 | jam.then (jam) ->
35 | callback
36 | type: ActionTypes.TOGGLE_NO_PIVOT
37 | jamId: jam.id
38 | .then (jam) ->
39 | expect(jam.noPivot).toBe(true)
40 | pit "toggles star pass", () ->
41 | jam.then (jam) ->
42 | callback
43 | type: ActionTypes.TOGGLE_STAR_PASS
44 | jamId: jam.id
45 | .then (jam) ->
46 | expect(jam.starPass).toBe(true)
47 | pit "sets a star pass for a specific pass", () ->
48 | jam.then (jam) ->
49 | callback
50 | type: ActionTypes.SET_STAR_PASS
51 | passId:
52 | jamId: jam.id
53 | passNumber: 1
54 | .then (jam) ->
55 | expect(jam.starPass).toBe(true)
56 | expect(jam.starPassNumber).toBe(1)
57 | expect(jam.passes.length).toBe(1)
58 | pit "sets and unsets a skater to a position", () ->
59 | jam.then (jam) ->
60 | callback
61 | type: ActionTypes.SET_SKATER_POSITION
62 | jamId: jam.id
63 | position: 'blocker2'
64 | skaterId: 'skater 1'
65 | .tap (jam) ->
66 | expect(jam.blocker2.id).toBe('skater 1')
67 | Skater.new.mockReturnValueOnce Promise.resolve(id: 'skater 1')
68 | .then (jam) ->
69 | callback
70 | type:ActionTypes.SET_SKATER_POSITION
71 | jamId: jam.id
72 | position: 'blocker2'
73 | skaterId: 'skater 1'
74 | .tap (jam) ->
75 | expect(jam.blocker2).toBe(null)
76 | pit "cycles a lineup status", () ->
77 | jam.then (jam) ->
78 | callback
79 | type: ActionTypes.CYCLE_LINEUP_STATUS
80 | jamId: jam.id
81 | statusIndex: 0
82 | position: 'pivot'
83 | .then (jam) ->
84 | expect(jam.lineupStatuses[0]['pivot']).toBe('went_to_box')
85 | pit "reorders passes", () ->
86 | spyOn(Jam.prototype, 'reorderPass')
87 | jam.then (jam) ->
88 | callback
89 | type: ActionTypes.REORDER_PASS
90 | jamId: jam.id
91 | sourcePassIndex: 0
92 | targetPassIndex: 1
93 | .then (jam) ->
94 | expect(jam.reorderPass).toHaveBeenCalledWith(0, 1)
95 | pit "creates a new pass", () ->
96 | jam.then (jam) ->
97 | callback
98 | type: ActionTypes.CREATE_NEXT_PASS
99 | jamId: jam.id
100 | passNumber: 2
101 | .then (jam) ->
102 | expect(jam.passes.length).toBe(2)
103 | pit "renumbers passes after one is removed", () ->
104 | jam.then (jam) ->
105 | Pass.dispatchToken = jamId: jam.id
106 | callback
107 | type: ActionTypes.REMOVE_PASS
108 | .then (jam) ->
109 | expect(AppDispatcher.waitFor).toBeCalled()
110 | expect(jam.passes[0].passNumber).toBe(1)
111 | expect(jam.passes[0].save).toBeCalled()
112 | pit "removes a jam", () ->
113 | jam.then (jam) ->
114 | callback
115 | type: ActionTypes.REMOVE_JAM
116 | jamId: jam.id
117 | .then () ->
118 | Jam.all()
119 | .then (jams) ->
120 | expect(jams.length).toBe(0)
121 | pit "does not create duplicate passes", () ->
122 | jam.then (jam) ->
123 | callback
124 | type: ActionTypes.CREATE_NEXT_PASS
125 | jamId: jam.id
126 | passNumber: 2
127 | .then (jam) ->
128 | callback
129 | type: ActionTypes.CREATE_NEXT_PASS
130 | jamId: jam.id
131 | passNumber: 2
132 | .then (jam) ->
133 | expect(jam.passes.length).toBe(2)
134 |
--------------------------------------------------------------------------------
/test/components/scoreboard-test.cjsx:
--------------------------------------------------------------------------------
1 | jest.dontMock('../../app/scripts/components/scoreboard')
2 | React = require 'react/addons'
3 | Scoreboard = require '../../app/scripts/components/scoreboard'
4 | TestUtils = React.addons.TestUtils
5 | DemoData = require '../../app/scripts/demo_data'
6 | Promise = require 'bluebird'
7 | describe 'Scoreboard', () ->
8 | process.setMaxListeners(0)
9 | gameState = null
10 | scoreboard = null
11 | container = document.createElement('div')
12 | beforeEach () ->
13 | gameState = DemoData.init()
14 | scoreboard = gameState.then (gameState) ->
15 | React.render , container
16 | afterEach () ->
17 | React.unmountComponentAtNode(container)
18 | pit 'renders a component', () ->
19 | scoreboard.then (scoreboard) ->
20 | expect(TestUtils.isDOMComponent(scoreboard.getDOMNode()))
21 | pit "displays the current period", () ->
22 | scoreboard.then (scoreboard) ->
23 | periodContainer = TestUtils.findRenderedDOMComponentWithClass(scoreboard, 'period-number')
24 | expect(periodContainer.getDOMNode().textContent).toEqual('Pre')
25 | pit "displays the current jam number", () ->
26 | scoreboard.then (scoreboard) ->
27 | jamContainer = TestUtils.findRenderedDOMComponentWithClass(scoreboard, 'jam-number')
28 | expect(jamContainer.getDOMNode().textContent).toEqual('0')
29 | pit "display jam points", () ->
30 | scoreboard.then (scoreboard) ->
31 | TestUtils.findRenderedDOMComponentWithClass(scoreboard, 'jam-points')
32 | .then (jamPoints) ->
33 | TestUtils.scryRenderedDOMComponentsWithClass(jamPoints, 'points')
34 | .spread (home, away) ->
35 | expect(home.getDOMNode().textContent).toEqual('0')
36 | expect(away.getDOMNode().textContent).toEqual('0')
37 | describe 'teams', () ->
38 | teams = null
39 | beforeEach () ->
40 | teams = scoreboard.then (scoreboard) -> TestUtils.scryRenderedDOMComponentsWithClass(scoreboard, 'team')
41 | pit "display names", () ->
42 | teams.spread (home, away) ->
43 | homeContainer = TestUtils.scryRenderedDOMComponentsWithClass(home, 'team-name')[0]
44 | awayContainer = TestUtils.scryRenderedDOMComponentsWithClass(away, 'team-name')[0]
45 | expect(homeContainer.getDOMNode().textContent).toEqual('Atlanta')
46 | expect(awayContainer.getDOMNode().textContent).toEqual('Gotham')
47 | pit "display logos", () ->
48 | teams.spread (home, away) ->
49 | homeContainer = TestUtils.findRenderedDOMComponentWithClass(home, 'logo')
50 | awayContainer = TestUtils.findRenderedDOMComponentWithClass(away, 'logo')
51 | expect(homeContainer.getDOMNode().firstChild.nodeName).toEqual('IMG')
52 | expect(awayContainer.getDOMNode().firstChild.nodeName).toEqual('IMG')
53 | pit "display game scores", () ->
54 | teams.spread (home, away) ->
55 | homeContainer = TestUtils.findRenderedDOMComponentWithClass(home, 'score')
56 | awayContainer = TestUtils.findRenderedDOMComponentWithClass(away, 'score')
57 | expect(homeContainer.getDOMNode().textContent).toEqual('0')
58 | expect(awayContainer.getDOMNode().textContent).toEqual('0')
59 | pit "display timeouts", () ->
60 | teams.spread (home, away) ->
61 | homeContainer = TestUtils.findRenderedDOMComponentWithClass(home, 'timeouts')
62 | awayContainer = TestUtils.findRenderedDOMComponentWithClass(away, 'timeouts')
63 | expect(homeContainer.getDOMNode().childNodes.length).toEqual(4)
64 | expect(awayContainer.getDOMNode().childNodes.length).toEqual(4)
65 | pit "display jammers with lead status", () ->
66 | teams.spread gameState, (home, away, gameState) ->
67 | gameState.home.jams[0].jammer = gameState.home.skaters[0]
68 | gameState.away.jams[0].jammer = gameState.away.skaters[0]
69 | gameState.home.jams[0].passes[0].lead = true
70 | gameState.jamNumber = 1
71 | scoreboard = TestUtils.renderIntoDocument
72 | home = TestUtils.findRenderedDOMComponentWithClass(scoreboard, 'team home')
73 | away = TestUtils.findRenderedDOMComponentWithClass(scoreboard, 'team away')
74 | homeContainer = TestUtils.findRenderedDOMComponentWithClass(home, 'jammer')
75 | homeName = TestUtils.findRenderedDOMComponentWithClass(homeContainer, 'name')
76 | homeLead = TestUtils.findRenderedDOMComponentWithClass(homeContainer, 'lead-status')
77 | awayContainer = TestUtils.findRenderedDOMComponentWithClass(away, 'jammer')
78 | awayName = TestUtils.findRenderedDOMComponentWithClass(awayContainer, 'name')
79 | awayLead = TestUtils.findRenderedDOMComponentWithClass(awayContainer, 'lead-status')
80 | expect(homeName.getDOMNode().textContent).toEqual("6 Wild Cherri")
81 | expect(homeLead.getDOMNode().firstChild.className).not.toContain('hidden')
82 | expect(awayName.getDOMNode().textContent).toEqual("00 Ana Bollocks")
83 | expect(awayLead.getDOMNode().firstChild.className).toContain('hidden')
84 |
--------------------------------------------------------------------------------
/app/styles/app.scss:
--------------------------------------------------------------------------------
1 | @import "colors";
2 | @import "base/variables";
3 | @import "bootstrap";
4 | @import "/bower_components/jquery-minicolors/jquery.minicolors.css";
5 | @import "mixins/border-arrow";
6 | @import "mixins/vertical-align";
7 | @import "mixins/gutters";
8 | @import "mixins/margins";
9 | @import "mixins/boxes";
10 | @import "header";
11 | @import "jam-timer";
12 | @import "lineup-tracker";
13 | @import "penalty-tracker";
14 | @import "scoreboard";
15 | @import "scorekeeper";
16 | @import "penalty-box-timer";
17 | @import "game-setup";
18 | @import "announcers-feed";
19 | @import "item-row";
20 | @import "skater-selector";
21 | @import "period-summary";
22 | //Some additional resets
23 | *:focus {
24 | outline: 0 !important;
25 | }
26 | //Hide test DOM
27 | #UUT {
28 | display: none;
29 | }
30 | .navbar {
31 | margin-bottom: 10px;
32 | border-radius: 0;
33 | }
34 |
35 | .bt-btn {
36 | @extend .btn;
37 | @extend .btn-block;
38 | @extend .btn-default;
39 | padding: 9px 2px;
40 | font-weight: bold;
41 | min-height: $minimum-touch-target;
42 | &.btn-selected {
43 | @include button-variant($btn-selected-color, $btn-selected-bg, $btn-selected-border);
44 | }
45 | &.btn-injury {
46 | @include button-variant($pink, $white, $pink);
47 | border-width: 5px;
48 | border-radius: 10px;
49 | padding: 5px 2px;
50 | }
51 | }
52 | .bt-box {
53 | @include box-variant($near-black, $white, $gray);
54 | padding: 9px 2px;
55 | font-weight: bold;
56 | min-height: $minimum-touch-target;
57 | &.box-default {
58 | @include box-variant($btn-default-color, $btn-default-bg, $btn-default-border);
59 | }
60 | &.box-primary {
61 | @include box-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border);
62 | }
63 | &.box-warning {
64 | @include box-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border);
65 | }
66 | &.box-danger {
67 | @include box-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border);
68 | }
69 | &.box-selected {
70 | @include box-variant($btn-selected-color, $btn-selected-bg, $btn-selected-border);
71 | }
72 | &.box-lg {
73 | padding: 0;
74 | font-size: 2.6em;
75 | @media (min-width: $screen-sm-min) {
76 | font-size: 2.8em;
77 | }
78 | @media (min-width: $screen-md-min) {
79 | font-size: 4.8em;
80 | }
81 | }
82 | }
83 | .bt-select {
84 | @extend .bt-box;
85 | display:block;
86 | &:after {
87 | content: '\e114';
88 | font-family: 'Glyphicons Halflings';
89 | position:absolute;
90 | top:16px;
91 | right:12px;
92 | z-index: 5;
93 | }
94 | select {
95 | -webkit-appearance: none;
96 | -moz-appearance: none;
97 | appearance: none;
98 | border: none;
99 | width:100%;
100 | z-index:10;
101 | cursor:pointer;
102 | background-color: transparent;
103 | position: relative;
104 | }
105 | }
106 | .top-buffer {
107 | margin-top: 4px;
108 | }
109 | .game{
110 | .gamename {
111 | margin-left: 1em;
112 | }
113 | .connection-status {
114 | font-size: 2em;
115 | position: absolute;
116 | top: 3px;
117 | margin-left: 10px;
118 | &.good-status {
119 | color: green;
120 | }
121 | &.bad-status {
122 | color: red;
123 | }
124 | }
125 | .login, .jam-timer, .lineup-tracker, .scorekeeper, .penalty-tracker, .penalty-box-timer, .scoreboard, .penalty-whiteboard, .announcers-feed, .game-setup {
126 | display: none;
127 | }
128 | .navbar {
129 | margin-bottom: 0px;
130 | }
131 | &[data-tab="jam_timer"] .jam-timer {
132 | display: block;
133 | }
134 | &[data-tab="lineup_tracker"] .lineup-tracker {
135 | display: block;
136 | }
137 | &[data-tab="scorekeeper"] .scorekeeper {
138 | display: block;
139 | }
140 | &[data-tab="penalty_tracker"] .penalty-tracker {
141 | display: block;
142 | }
143 | &[data-tab="penalty_box_timer"] .penalty-box-timer {
144 | display: block;
145 | }
146 | &[data-tab="scoreboard"] .scoreboard {
147 | display: block;
148 | @media (max-width: -1px + $screen-tablet){
149 | display: flex;
150 | }
151 | }
152 | &[data-tab="penalty_whiteboard"] .penalty-whiteboard {
153 | display: block;
154 | }
155 | &[data-tab="announcers_feed"] .announcers-feed {
156 | display: block;
157 | }
158 | &[data-tab="game_setup"] .game-setup {
159 | display: block;
160 | }
161 | &[data-tab="login"] .login {
162 | display: block;
163 | }
164 | }
165 | .col-xs-5-cols {
166 | $width: 100%/5.0;
167 | float:left;
168 | width: $width;
169 | }
170 | .col-xs-7-cols {
171 | $width: 100%/7.0;
172 | float:left;
173 | width: $width;
174 | }
175 | .row.gutters-xs {
176 | @include gutters-xs;
177 | }
178 | .modal {
179 | -webkit-overflow-scrolling: auto;
180 | }
181 | .fade-hide {
182 | position:absolute;
183 | left:2px;
184 | right:2px;
185 | opacity: 0;
186 | visibility: hidden;
187 | transition: opacity 0.15s, visibility 0.15s;
188 | &.in {
189 | opacity: 1;
190 | visibility: visible;
191 | transition-delay: 0.15s;
192 | }
193 | }
194 | .clickable {
195 | cursor: pointer;
196 | }
197 | .margin-xs {
198 | @include margin-xs;
199 | }
200 | @keyframes active-animation {
201 | 0% { background-color: $inactive-color; }
202 | 50% { background-color: $alert-color; }
203 | 100% { background-color: $inactive-color; }
204 | }
205 |
206 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var gulp = require('gulp');
3 | var del = require('del');
4 | var spawn = require('child_process').spawn;
5 |
6 | // Load plugins
7 | var $ = require('gulp-load-plugins')();
8 | var browserify = require('browserify');
9 | var watchify = require('watchify');
10 | var source = require('vinyl-source-stream');
11 | var mainBowerFiles = require('main-bower-files');
12 |
13 | var sourceFile = './app/scripts/app.cjsx';
14 | var destFolder = './dist/scripts';
15 | var destFileName = 'app.js';
16 | var server;
17 |
18 | // Styles
19 | gulp.task('styles', ['sass']);
20 |
21 | gulp.task('sass', function() {
22 | return gulp.src(['app/styles/**/*.scss', 'app/styles/**/*.css'])
23 | .pipe($.sass({
24 | errLogToConsole: true,
25 | includePaths: [
26 | 'app/bower_components/bootstrap-sass/assets/stylesheets',
27 | 'app/bower_components/jquery-minicolors'
28 | ]
29 | }))
30 | .pipe($.autoprefixer('last 1 version'))
31 | .pipe(gulp.dest('dist/styles'))
32 | .pipe($.size());
33 | });
34 |
35 | var bundler = browserify({
36 | entries: [sourceFile],
37 | debug: true,
38 | verbose: true,
39 | insertGlobals: true,
40 | cache: {},
41 | packageCache: {},
42 | fullPaths: true,
43 | extensions: ['.coffee', '.cjsx']
44 | });
45 |
46 | bundler.on('update', rebundle);
47 | bundler.on('log', $.util.log);
48 |
49 | function rebundle() {
50 | return bundler.bundle()
51 | // log errors if they happen
52 | .on('error', $.util.log.bind($.util, 'Browserify Error'))
53 | .pipe(source(destFileName))
54 | .pipe(gulp.dest(destFolder));
55 | }
56 |
57 | function watchBundle() {
58 | return watchify(bundler).bundle()
59 | // log errors if they happen
60 | .on('error', $.util.log.bind($.util, 'Browserify Error'))
61 | .pipe(source(destFileName))
62 | .pipe(gulp.dest(destFolder));
63 | }
64 |
65 | gulp.task('coffee', function() {
66 | return gulp.src('./app/**/*.coffee')
67 | .pipe($.coffee({bare: true}).on('error', $.util.log))
68 | .pipe(gulp.dest('./dist/'));
69 | });
70 | // Scripts
71 | gulp.task('scripts', ['coffee'], rebundle);
72 |
73 | // HTML
74 | gulp.task('html', function() {
75 | return gulp.src('app/*.html')
76 | .pipe($.useref())
77 | .pipe(gulp.dest('dist'))
78 | .pipe($.size());
79 | });
80 |
81 | // Images
82 | gulp.task('images', function() {
83 | return gulp.src('app/images/**/*')
84 | .pipe(gulp.dest('dist/images'))
85 | .pipe($.size());
86 | });
87 |
88 | // Fonts
89 | gulp.task('fonts', function() {
90 | return gulp.src(mainBowerFiles({
91 | filter: '**/*.{eot,svg,ttf,woff,woff2}'
92 | }).concat('app/fonts/**/*'))
93 | .pipe(gulp.dest('dist/fonts'));
94 | });
95 |
96 | // Clean
97 | gulp.task('clean', function(done) {
98 | $.cache.clearAll();
99 | del.sync(['dist/*'], done);
100 | });
101 |
102 | // Bundle
103 | gulp.task('bundle', ['styles', 'scripts', 'bower'], function() {
104 | return gulp.src('./app/*.html')
105 | .pipe($.useref.assets())
106 | .pipe($.useref.assets().restore())
107 | .pipe($.useref())
108 | .pipe(gulp.dest('dist'));
109 | });
110 |
111 | // Bower helper
112 | gulp.task('bower', function() {
113 | return gulp.src([
114 | 'app/bower_components/**/*.js',
115 | 'app/bower_components/**/*.map',
116 | 'app/bower_components/**/*.css'
117 | ], {
118 | base: 'app/bower_components'
119 | })
120 | .pipe(gulp.dest('dist/bower_components/'));
121 | });
122 |
123 | gulp.task('json', function() {
124 | return gulp.src('app/scripts/json/**/*.json', {
125 | base: 'app/scripts'
126 | })
127 | .pipe(gulp.dest('dist/scripts/'));
128 | });
129 |
130 | // Robots.txt and favicon.ico
131 | gulp.task('extras', function() {
132 | return gulp.src(['app/*.txt', 'app/*.ico'])
133 | .pipe(gulp.dest('dist/'))
134 | .pipe($.size());
135 | });
136 |
137 | // Uglify
138 | gulp.task('uglify', ['scripts'], function() {
139 | return gulp.src('dist/scripts/app.js')
140 | .pipe($.uglify({mangle: false}))
141 | .pipe($.stripDebug())
142 | .pipe(gulp.dest('dist/scripts'));
143 | });
144 |
145 | // Build
146 | gulp.task('build', ['html', 'bundle', 'images', 'fonts', 'extras']);
147 |
148 | // Package
149 | gulp.task('package', ['clean', 'build', 'uglify']);
150 |
151 | // Watch
152 | gulp.task('watch', ['build'], function() {
153 | var startServer = gulp.start.bind(this, 'server');
154 | startServer();
155 |
156 | gulp.watch('app/*.html', ['html'], startServer);
157 |
158 | gulp.watch(['app/styles/**/*.scss', 'app/styles/**/*.css'], ['styles'], startServer);
159 |
160 | gulp.watch(['app/**/*.coffee'], ['coffee'], startServer);
161 |
162 | gulp.watch('app/images/**/*', ['images'], startServer);
163 |
164 | gulp.watch('app/fonts/**/*', ['fonts'], startServer);
165 |
166 | gulp.watch(['app/*.txt', 'app/*.ico'], ['extras'], startServer);
167 |
168 | watchBundle();
169 | });
170 |
171 | // Dev server stop and restart
172 | gulp.task('server', function(done) {
173 | if (server) {
174 | server.kill();
175 | }
176 | server = spawn('node', ['./bin/bouttime-server'], {
177 | stdio: 'inherit'
178 | });
179 | server.on('close', function(code) {
180 | if (code === 8) {
181 | gulp.log('Error detected, waiting for changes...');
182 | }
183 | });
184 | done();
185 | });
186 |
187 | process.on('exit', function() {
188 | if (server) {
189 | server.kill();
190 | }
191 | });
192 |
193 | // Default task
194 | gulp.task('default', ['clean', 'build']);
195 |
--------------------------------------------------------------------------------
/app/scripts/clock.coffee:
--------------------------------------------------------------------------------
1 | moment = require 'moment'
2 | require 'moment-duration-format'
3 | functions = require './functions'
4 | constants = require './constants'
5 | EventEmitter = require('events').EventEmitter
6 | module.exports =
7 | ClockManager: class ClockManager
8 | instance = null
9 | constructor: (options = {}) ->
10 | if instance
11 | return instance
12 | else
13 | instance = this
14 | @clocks = {}
15 | @lastTick = null
16 | @listeners = []
17 | @refreshRateInMS = options.refreshRateInMs ? constants.CLOCK_REFRESH_RATE_IN_MS
18 | @emitter = new EventEmitter()
19 | @initialize()
20 | initialize: () ->
21 | @lastTick = Date.now()
22 | exports.clockManagerInterval = setInterval(() =>
23 | @tick()
24 | ,@refreshRateInMS)
25 | destroy: () ->
26 | clearInterval exports.clockManagerInterval
27 | exports.clockManagerInterval = null
28 | addClock: (alias, options = {}) =>
29 | options.alias = alias
30 | clock = new Clock(options)
31 | @clocks[alias] = clock
32 | removeClock: (alias) ->
33 | delete @clocks[alias]
34 | getClock: (alias) ->
35 | @clocks[alias]
36 | getOrAddClock: (alias, options = {}) =>
37 | @getClock(alias) ? @addClock(alias, options)
38 | addTickListener: (listenerFunction) ->
39 | @emitter.on("masterTick", listenerFunction)
40 | removeTickListener: (listenerFunction) ->
41 | @emitter.removeListener("masterTick", listenerFunction)
42 | tick: () ->
43 | tick = Date.now()
44 | delta = tick - @lastTick
45 | @lastTick = tick
46 | for alias, clock of @clocks
47 | clock.tick(delta)
48 | @issueTick()
49 | issueTick: () ->
50 | @emitter.emit("masterTick")
51 | Clock: class Clock
52 | constructor: (options = {}) ->
53 | @id = functions.uniqueId()
54 | @alias = options.alias
55 | @emitter = new EventEmitter()
56 | @undoStack = if options.undoStack? then new Clock(options.undoStack) else null
57 | @redoStack = if options.redoStack? then new Clock(options.redoStack) else null
58 | @dummyUndo = options.dummyUndo ? 0
59 | @dummyRedo = options.dummyRedo ? 0
60 | @sync (options)
61 | start: () =>
62 | @clearRedo()
63 | @_pushUndo()
64 | unless @isRunning
65 | @isRunning = true
66 | @lastTick = Date.now()
67 | stop: () =>
68 | @clearRedo()
69 | @_pushUndo()
70 | @isRunning = false
71 | toggle: () =>
72 | if @isRunning
73 | @stop()
74 | else
75 | @start()
76 | sync: (options={}) ->
77 | @isRunning = options.isRunning ? false
78 | @warningIssued = options.warningIssued ? false
79 | @expirationIssued = options.expirationIssued ? false
80 | @tickUp = options.tickUp ? false
81 | @refreshRateInMS = options.refreshRateInMs ? constants.CLOCK_REFRESH_RATE_IN_MS
82 | @time = @parse(options.time)
83 | @warningTime = options.warningTime ? null
84 | @lastTick = Date.now()
85 | reset: (options) ->
86 | @clearRedo()
87 | if options?
88 | @_pushUndo()
89 | @sync options
90 | else
91 | @dummyUndo++
92 | undo: () ->
93 | if @dummyUndo > 0
94 | @dummyUndo--
95 | @dummyRedo++
96 | else
97 | @_pushRedo()
98 | @_popUndo()
99 | redo: () ->
100 | if @dummyRedo > 0
101 | @dummyRedo--
102 | @dummyUndo++
103 | else
104 | @_pushUndo()
105 | @_popRedo()
106 | isUndoable: () ->
107 | @undoStack? or @dummyUndo > 0
108 | isRedoable: () ->
109 | @redoStack? or @dummyRedo > 0
110 | clearUndo: () ->
111 | @undoStack = null
112 | clearRedo: () ->
113 | @redoStack = null
114 | _pushUndo: () ->
115 | @undoStack = new Clock(this)
116 | @undoStack.clearRedo()
117 | _popUndo: () ->
118 | @sync @undoStack
119 | @undoStack = @undoStack.undoStack
120 | _pushRedo: () ->
121 | @redoStack = new Clock(this)
122 | @redoStack.clearUndo()
123 | _popRedo: () ->
124 | @sync @redoStack
125 | @redoStack = @redoStack.redoStack
126 | display: () =>
127 | moment.duration(@time).format('mm:ss')
128 | parse: (time) =>
129 | switch typeof time
130 | when 'string'
131 | parts = time.split(':')
132 | while parts.length < 3
133 | parts.unshift('00')
134 | time = parts.join(':')
135 | moment.duration(time).asMilliseconds()
136 | when 'number'
137 | time
138 | else
139 | 0
140 | issueExpiration: () =>
141 | @expirationIssued = true
142 | if @emitter
143 | @emitter.emit("clockExpiration")
144 | issueWarning: () =>
145 | @warningIssued = true
146 | if @emitter
147 | @emitter.emit("clockWarning")
148 | issueTick: () =>
149 | if @emitter
150 | @emitter.emit("clockTick")
151 | tick: (delta) ->
152 | if @isRunning
153 | # Synchronize with master tick
154 | if @lastTick
155 | delta = (Date.now() - @lastTick)
156 | @lastTick = null
157 | # Adjust time
158 | @time = if @tickUp then @time + delta else @time - delta
159 | @time = 0 if @time < 0
160 | if !@warningIssued && @warningTime && @time <= @warningTime
161 | @issueWarning()
162 | if !@expirationIssued && @time == 0
163 | @issueExpiration()
164 | @issueTick()
165 | @undoStack?.tick(delta)
166 | @redoStack?.tick(delta)
--------------------------------------------------------------------------------