├── app ├── images │ ├── .keep │ ├── team_logos │ │ ├── .version │ │ ├── DC.png │ │ ├── NRV.gif │ │ ├── Boston.png │ │ ├── Denver.png │ │ ├── Dixie.png │ │ ├── Gotham.png │ │ ├── Kansas.png │ │ ├── Philly.png │ │ ├── Rogue.png │ │ ├── Texas.png │ │ ├── Atlanta.png │ │ ├── BayArea.png │ │ ├── Cape Fear.png │ │ ├── Carolina.png │ │ ├── Charlotte.png │ │ ├── CharmCity.png │ │ ├── Columbia.png │ │ ├── Detroit.png │ │ ├── Dominion.png │ │ ├── Dutchland.png │ │ ├── Houston.png │ │ ├── Madison.png │ │ ├── Montreal.png │ │ ├── Palmetto.png │ │ ├── RatCity.png │ │ ├── RiverCity.png │ │ ├── RoseCity.png │ │ ├── San Diego.png │ │ ├── SteelCity.png │ │ ├── WindyCity.png │ │ ├── Blue Ridge.png │ │ ├── Connecticut.png │ │ ├── Hurticanes.png │ │ ├── OlyRollers.png │ │ ├── Providence.png │ │ ├── Tallahassee.png │ │ ├── RockyMountain.png │ │ ├── Carolina-Bootleggers.png │ │ ├── Carolina-Tai Chi-Tahs.png │ │ ├── Carolina-Trauma Queens.png │ │ ├── Garden State Rollergirls.png │ │ ├── Gold Coast Derby Grrls.png │ │ ├── Richland County Regulators.png │ │ ├── Carolina-Debutante Brawlers.png │ │ └── Tallahassee-Jailbreak Betties.png │ ├── logo.png │ └── icons │ │ └── menu.svg ├── scripts │ ├── config.coffee │ ├── memory_storage.coffee │ ├── app.cjsx │ ├── components │ │ ├── login.cjsx │ │ ├── game_notes.cjsx │ │ ├── shared │ │ │ ├── shortcut_button.cjsx │ │ │ ├── connection_status.cjsx │ │ │ ├── jam_summary.cjsx │ │ │ ├── period_summary.cjsx │ │ │ ├── skater_selector.cjsx │ │ │ ├── team_selector.cjsx │ │ │ ├── skater_selector_modal.cjsx │ │ │ └── item_row.cjsx │ │ ├── penalty_whiteboard.cjsx │ │ ├── scorekeeper │ │ │ ├── score_note.cjsx │ │ │ ├── jam_item.cjsx │ │ │ ├── jam_details.cjsx │ │ │ ├── pass_item.cjsx │ │ │ ├── passes_list.cjsx │ │ │ └── jams_list.cjsx │ │ ├── lineup_tracker │ │ │ ├── team_lineup.cjsx │ │ │ ├── lineup_box_row.cjsx │ │ │ ├── lineup_tracker_actions.cjsx │ │ │ ├── lineup_box.cjsx │ │ │ ├── lineup_selector.cjsx │ │ │ └── jam_detail.cjsx │ │ ├── penalty_tracker │ │ │ ├── penalty_alert.cjsx │ │ │ ├── penalty_control.cjsx │ │ │ ├── penalty_indicator.cjsx │ │ │ ├── penalties_list.cjsx │ │ │ ├── penalties_summary.cjsx │ │ │ ├── team_penalties.cjsx │ │ │ └── edit_penalty_panel.cjsx │ │ ├── jam_timer │ │ │ └── timeout_bars.cjsx │ │ ├── penalty_tracker.cjsx │ │ ├── scoreboard │ │ │ ├── scoreboard_ads.cjsx │ │ │ ├── scoreboard_alerts.cjsx │ │ │ ├── scoreboard_clocks.cjsx │ │ │ └── scoreboard_team.cjsx │ │ ├── scorekeeper.cjsx │ │ ├── announcers_feed.cjsx │ │ ├── titlebar.cjsx │ │ ├── announcers_feed │ │ │ ├── feed_lineup_team.cjsx │ │ │ └── feed_item.cjsx │ │ ├── penalty_box_timer.cjsx │ │ ├── lineup_tracker.cjsx │ │ ├── penalty_box_timer │ │ │ ├── team_penalty_timers.cjsx │ │ │ └── penalty_clock.cjsx │ │ ├── scoreboard.cjsx │ │ ├── game_setup │ │ │ ├── roster_fields.cjsx │ │ │ ├── team_fields.cjsx │ │ │ └── game_form.cjsx │ │ ├── game_setup.cjsx │ │ ├── game_picker.cjsx │ │ └── navbar.cjsx │ ├── models │ │ ├── __mocks__ │ │ │ ├── team.coffee │ │ │ ├── pass.coffee │ │ │ ├── box_entry.coffee │ │ │ ├── jam.coffee │ │ │ └── skater.coffee │ │ ├── game_metadata.coffee │ │ ├── skater.coffee │ │ ├── store.coffee │ │ ├── box_entry.coffee │ │ └── pass.coffee │ ├── dispatcher │ │ ├── __mocks__ │ │ │ └── app_dispatcher.coffee │ │ └── app_dispatcher.coffee │ ├── __mocks__ │ │ └── clock.coffee │ ├── namespaces.coffee │ ├── functions.coffee │ ├── shallowCompare.js │ ├── shallowEqual.js │ ├── constants.coffee │ ├── demo_data.coffee │ └── clock.coffee ├── styles │ ├── _period-summary.scss │ ├── mixins │ │ ├── _vertical-align.scss │ │ ├── _boxes.scss │ │ ├── _margins.scss │ │ ├── _gutters.scss │ │ └── _border-arrow.scss │ ├── _announcers-feed.scss │ ├── _lineup-tracker.scss │ ├── _skater-selector.scss │ ├── _game-setup.scss │ ├── _scorekeeper.scss │ ├── _colors.scss │ ├── _item-row.scss │ ├── _penalty-box-timer.scss │ ├── _jam-timer.scss │ ├── _header.scss │ ├── _penalty-tracker.scss │ ├── base │ │ └── _variables.scss │ └── app.scss ├── index.html └── server.coffee ├── .eslintignore ├── circle.yml ├── .bowerrc ├── .csslintrc ├── .npmignore ├── docker-compose.yml ├── .github ├── PULL_REQUEST_TEMPLATE ├── ISSUE_TEMPLATE └── CONTRIBUTING.md ├── .codeclimate.yml ├── .gitignore ├── .dockerignore ├── bin └── bouttime-server ├── bower.json ├── preprocessor.js ├── Dockerfile ├── setup.sh ├── LICENSE ├── test ├── components │ ├── scorekeeper-test.cjsx │ ├── penalty_tracker-test.cjsx │ ├── lineup_tracker-test.cjsx │ └── scoreboard-test.cjsx └── models │ ├── box_entry-test.coffee │ ├── skater-test.coffee │ ├── pass-test.coffee │ ├── team-test.coffee │ └── jam-test.coffee ├── .eslintrc ├── README.md ├── demo.json ├── CODE_OF_CONDUCT.md ├── package.json └── gulpfile.js /app/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/images/team_logos/.version: -------------------------------------------------------------------------------- 1 | version=1 2 | -------------------------------------------------------------------------------- /app/scripts/config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = {} -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | app/bower_components/* 3 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.10.47 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /app/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/logo.png -------------------------------------------------------------------------------- /app/images/team_logos/DC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/DC.png -------------------------------------------------------------------------------- /app/images/team_logos/NRV.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/NRV.gif -------------------------------------------------------------------------------- /app/images/team_logos/Boston.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Boston.png -------------------------------------------------------------------------------- /app/images/team_logos/Denver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Denver.png -------------------------------------------------------------------------------- /app/images/team_logos/Dixie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Dixie.png -------------------------------------------------------------------------------- /app/images/team_logos/Gotham.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Gotham.png -------------------------------------------------------------------------------- /app/images/team_logos/Kansas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Kansas.png -------------------------------------------------------------------------------- /app/images/team_logos/Philly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Philly.png -------------------------------------------------------------------------------- /app/images/team_logos/Rogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Rogue.png -------------------------------------------------------------------------------- /app/images/team_logos/Texas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Texas.png -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | --exclude-exts=.min.css 2 | --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes 3 | -------------------------------------------------------------------------------- /app/images/team_logos/Atlanta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Atlanta.png -------------------------------------------------------------------------------- /app/images/team_logos/BayArea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/BayArea.png -------------------------------------------------------------------------------- /app/images/team_logos/Cape Fear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Cape Fear.png -------------------------------------------------------------------------------- /app/images/team_logos/Carolina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Carolina.png -------------------------------------------------------------------------------- /app/images/team_logos/Charlotte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Charlotte.png -------------------------------------------------------------------------------- /app/images/team_logos/CharmCity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/CharmCity.png -------------------------------------------------------------------------------- /app/images/team_logos/Columbia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Columbia.png -------------------------------------------------------------------------------- /app/images/team_logos/Detroit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Detroit.png -------------------------------------------------------------------------------- /app/images/team_logos/Dominion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Dominion.png -------------------------------------------------------------------------------- /app/images/team_logos/Dutchland.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Dutchland.png -------------------------------------------------------------------------------- /app/images/team_logos/Houston.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Houston.png -------------------------------------------------------------------------------- /app/images/team_logos/Madison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Madison.png -------------------------------------------------------------------------------- /app/images/team_logos/Montreal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Montreal.png -------------------------------------------------------------------------------- /app/images/team_logos/Palmetto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Palmetto.png -------------------------------------------------------------------------------- /app/images/team_logos/RatCity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/RatCity.png -------------------------------------------------------------------------------- /app/images/team_logos/RiverCity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/RiverCity.png -------------------------------------------------------------------------------- /app/images/team_logos/RoseCity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/RoseCity.png -------------------------------------------------------------------------------- /app/images/team_logos/San Diego.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/San Diego.png -------------------------------------------------------------------------------- /app/images/team_logos/SteelCity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/SteelCity.png -------------------------------------------------------------------------------- /app/images/team_logos/WindyCity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/WindyCity.png -------------------------------------------------------------------------------- /app/images/team_logos/Blue Ridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Blue Ridge.png -------------------------------------------------------------------------------- /app/images/team_logos/Connecticut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Connecticut.png -------------------------------------------------------------------------------- /app/images/team_logos/Hurticanes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Hurticanes.png -------------------------------------------------------------------------------- /app/images/team_logos/OlyRollers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/OlyRollers.png -------------------------------------------------------------------------------- /app/images/team_logos/Providence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Providence.png -------------------------------------------------------------------------------- /app/images/team_logos/Tallahassee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Tallahassee.png -------------------------------------------------------------------------------- /app/images/team_logos/RockyMountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/RockyMountain.png -------------------------------------------------------------------------------- /app/styles/_period-summary.scss: -------------------------------------------------------------------------------- 1 | .period-summary { 2 | .box-clock { 3 | color: $muted-gray; 4 | font-weight: lighter; 5 | } 6 | } -------------------------------------------------------------------------------- /app/styles/mixins/_vertical-align.scss: -------------------------------------------------------------------------------- 1 | @mixin vertical-align() { 2 | position: relative; 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } -------------------------------------------------------------------------------- /app/images/team_logos/Carolina-Bootleggers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Carolina-Bootleggers.png -------------------------------------------------------------------------------- /app/images/team_logos/Carolina-Tai Chi-Tahs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Carolina-Tai Chi-Tahs.png -------------------------------------------------------------------------------- /app/images/team_logos/Carolina-Trauma Queens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Carolina-Trauma Queens.png -------------------------------------------------------------------------------- /app/images/team_logos/Garden State Rollergirls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Garden State Rollergirls.png -------------------------------------------------------------------------------- /app/images/team_logos/Gold Coast Derby Grrls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Gold Coast Derby Grrls.png -------------------------------------------------------------------------------- /app/images/team_logos/Richland County Regulators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Richland County Regulators.png -------------------------------------------------------------------------------- /app/images/team_logos/Carolina-Debutante Brawlers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Carolina-Debutante Brawlers.png -------------------------------------------------------------------------------- /app/images/team_logos/Tallahassee-Jailbreak Betties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerbyBoutTime/bouttime/HEAD/app/images/team_logos/Tallahassee-Jailbreak Betties.png -------------------------------------------------------------------------------- /app/styles/_announcers-feed.scss: -------------------------------------------------------------------------------- 1 | .announcers-feed { 2 | margin-top:10px; 3 | .feed-lineup{ 4 | border-right: 1px solid $near-black; 5 | padding-right: 4px; 6 | } 7 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | capybara-*.html 3 | **.orig 4 | rerun.txt 5 | pickle-email-*.html 6 | .sass-cache/ 7 | setup.sh 8 | /app/ 9 | /scratch/ 10 | bower.json 11 | gulpfile.js 12 | -------------------------------------------------------------------------------- /app/scripts/memory_storage.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class MemoryStorage 2 | setItem: (id, val) -> this[id] = val 3 | getItem: (id) -> this[id] 4 | removeItem: (id) -> delete this[id] -------------------------------------------------------------------------------- /app/styles/mixins/_boxes.scss: -------------------------------------------------------------------------------- 1 | @mixin box-variant($color, $bg, $border) { 2 | color: $color; 3 | background-color: $bg; 4 | border: 1px solid $border; 5 | border-radius: 4px; 6 | } -------------------------------------------------------------------------------- /app/styles/_lineup-tracker.scss: -------------------------------------------------------------------------------- 1 | .lineup-tracker { 2 | .lineup-box { 3 | @include button-variant($gray, $white, $gray); 4 | &.injury { 5 | color: $pink; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/styles/mixins/_margins.scss: -------------------------------------------------------------------------------- 1 | @mixin margin-xs() { 2 | margin-top: $padding-base-horizontal/2; 3 | @media (min-width: $screen-tablet){ 4 | margin-top: $padding-base-horizontal; 5 | } 6 | } -------------------------------------------------------------------------------- /app/scripts/app.cjsx: -------------------------------------------------------------------------------- 1 | $ = require 'jquery' 2 | require 'bootstrap' 3 | React = require 'react' 4 | GamePicker = require './components/game_picker.cjsx' 5 | React.render , document.getElementById('react') -------------------------------------------------------------------------------- /app/styles/mixins/_gutters.scss: -------------------------------------------------------------------------------- 1 | @mixin gutters-xs() { 2 | margin-right: -2px; 3 | margin-left: -2px; 4 | [class^="col-"], [class*=" col-"] { 5 | padding-right: 2px; 6 | padding-left: 2px; 7 | } 8 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | bouttime: 5 | build: ./ 6 | ports: 7 | - 3000:3000 8 | volumes: 9 | - ./app/:/opt/bouttime/app/ 10 | command: npm run watch 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | Description of changes 2 | 3 | Fixes #. 4 | References #. 5 | 6 | Changes proposed in this PR: 7 | 8 | - 9 | - 10 | - 11 | 12 | Code review: @WFTDA/bouttime, @WFTDA/code-monkeys 13 | -------------------------------------------------------------------------------- /app/scripts/components/login.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react/addons' 2 | cx = React.addons.classSet 3 | module.exports = React.createClass 4 | render: () -> 5 |
6 | Login 7 |
-------------------------------------------------------------------------------- /app/styles/_skater-selector.scss: -------------------------------------------------------------------------------- 1 | .bt-btn.btn-selector { 2 | @include button-variant($dark-gray, $light-gray, $light-gray); 3 | &.selected { 4 | color: $near-black; 5 | } 6 | .glyphicon { 7 | margin: 0px 4px; 8 | } 9 | } -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: true 4 | csslint: 5 | enabled: true 6 | ratings: 7 | paths: 8 | - "app/**" 9 | exclude_paths: 10 | - "app/bower_components/**" #Paths that you want excluded from our analysis. 11 | -------------------------------------------------------------------------------- /app/scripts/models/__mocks__/team.coffee: -------------------------------------------------------------------------------- 1 | Promise = require.requireActual('bluebird') 2 | teamMock = jest.genMockFromModule('../team') 3 | teamMock.findOrCreate.mockImplementation (opts) -> 4 | Promise.resolve(new teamMock(opts)) 5 | module.exports = teamMock -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # misc 2 | .DS_Store 3 | *.rbc 4 | *.swp 5 | **.orig 6 | 7 | rerun.txt 8 | pickle-email-*.html 9 | 10 | # npm 11 | node_modules/ 12 | npm-debug.log 13 | /dist/ 14 | 15 | # app 16 | bower_components/ 17 | .sass-cache/ 18 | *.db 19 | /scratch/ 20 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !Dockerfile 3 | !package.json 4 | !npm-shrinkwrap.json 5 | !bower.json 6 | !bin/* 7 | !app/* 8 | !gulpfile.js 9 | !.bowerrc 10 | !.csslintrc 11 | !.eslintignore 12 | !.eslintrc 13 | !.npmignore 14 | !README.md 15 | !LICENSE 16 | !preprocessor.js 17 | -------------------------------------------------------------------------------- /app/scripts/dispatcher/__mocks__/app_dispatcher.coffee: -------------------------------------------------------------------------------- 1 | Promise = require.requireActual('bluebird') 2 | dispatcherMock = jest.genMockFromModule('../app_dispatcher') 3 | dispatcherMock.waitFor.mockImplementation (ids) -> 4 | Promise.resolve ids 5 | module.exports = dispatcherMock -------------------------------------------------------------------------------- /bin/bouttime-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | server = require ('../dist/server'); 3 | program = require ('commander'); 4 | 5 | program 6 | .version('0.0.30') 7 | .option('-p, --port [port]', 'Specify a port to listen on') 8 | .parse(process.argv) 9 | 10 | server.start(program.port) 11 | -------------------------------------------------------------------------------- /app/scripts/__mocks__/clock.coffee: -------------------------------------------------------------------------------- 1 | clockMock = jest.genMockFromModule('../clock') 2 | clockMock.ClockManager.prototype.getOrAddClock.mockImplementation () -> 3 | new clockMock.Clock() 4 | clockMock.ClockManager.prototype.addClock.mockImplementation () -> 5 | new clockMock.Clock() 6 | module.exports = clockMock -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bouttime", 3 | "version": "0.0.30", 4 | "dependencies": { 5 | "jquery": "~2.1.4", 6 | "bootstrap-sass": "~3.3.4", 7 | "eonasdan-bootstrap-datetimepicker": "~4.7.14", 8 | "jquery-minicolors": "~2.1.12", 9 | "moment-duration-format": "~1.3.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/scripts/components/game_notes.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react/addons' 2 | $ = require 'jquery' 3 | cx = React.addons.classSet 4 | module.exports = React.createClass 5 | componentDidMount: () -> 6 | $dom = $(@getDOMNode()) 7 | getInitialState: () -> 8 | @props 9 | render: () -> 10 |
11 | -------------------------------------------------------------------------------- /app/scripts/namespaces.coffee: -------------------------------------------------------------------------------- 1 | exports = exports ? this 2 | exports.wftda = exports.wftda ? {} 3 | exports.wftda.classes = exports.wftda.classes ? {} 4 | exports.wftda.ticks = exports.wftda.ticks ? {} 5 | exports.wftda.functions = exports.wftda.functions ? {} 6 | exports.wftda.components = exports.wftda.components ? {} 7 | exports.wftda.constants = exports.wftda.constants ? {} -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BoutTime 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/styles/_game-setup.scss: -------------------------------------------------------------------------------- 1 | .game-setup { 2 | h3 { 3 | text-transform: capitalize; 4 | } 5 | .logo { 6 | display:block; 7 | margin:15px auto 0 auto; 8 | max-width:100%; 9 | max-height:200px; 10 | } 11 | .ad { 12 | display:block; 13 | margin: 15px auto 0 auto; 14 | max-width: 100%; 15 | max-height: 90px; 16 | } 17 | hr { 18 | border-top: 1px solid $gray; 19 | } 20 | } -------------------------------------------------------------------------------- /preprocessor.js: -------------------------------------------------------------------------------- 1 | var coffee = require('coffee-script'); 2 | var transform = require('coffee-react-transform'); 3 | 4 | module.exports = { 5 | process: function(src, path) { 6 | // CoffeeScript files can be .coffee, .litcoffee, or .coffee.md 7 | if (coffee.helpers.isCoffee(path) || (path.match(/\.cjsx/))) { 8 | return coffee.compile(transform(src), {'bare': true}); 9 | } 10 | 11 | return src; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:0.10 2 | 3 | RUN mkdir -p /opt/bouttime 4 | WORKDIR /opt/bouttime 5 | 6 | RUN echo '{ "allow_root": true }' > /root/.bowerrc 7 | ADD package.json npm-shrinkwrap.json ./ 8 | RUN npm install && npm cache clean 9 | COPY bower.json .bowerrc ./ 10 | 11 | COPY ./gulpfile.js . 12 | COPY ./app/ ./app/ 13 | COPY ./bin/ ./bin/ 14 | 15 | RUN npm run build 16 | 17 | VOLUME /opt/bouttime/ 18 | EXPOSE 3000 19 | 20 | CMD ["./bin/bouttime-server"] 21 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | echo "installing bower and gulp globally" 6 | npm install -g bower gulp 7 | 8 | echo "installing NPM dependencies" 9 | npm install 10 | 11 | echo "installing bower dependencies" 12 | bower install 13 | 14 | echo "Dependencies have been installed." 15 | echo "You can now run gulp to build the app and gulp watch to build as you make changes." 16 | echo "Start the BoutTime server with ./bin/bouttime-server" 17 | -------------------------------------------------------------------------------- /app/styles/_scorekeeper.scss: -------------------------------------------------------------------------------- 1 | .scorekeeper { 2 | // because collapse requires a .panel to work with data-parent, 3 | // https://github.com/twbs/bootstrap/blob/a577f1922ee7054a97b455ccfaa16857cb2b79fc/js/collapse.js#L49, 4 | // but we don't want the .panel styles. 5 | .panel { 6 | border: 0; 7 | margin-bottom: 0; 8 | } 9 | .edit-pass { 10 | @include border-arrow(92%); 11 | .btn-score { 12 | @include button-variant($near-black, $white, $white); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/scripts/components/shared/shortcut_button.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react/addons' 2 | Mousetrap = require 'mousetrap' 3 | module.exports = React.createClass 4 | displayName: 'ShortcutButton' 5 | propTypes: 6 | shortcut: React.PropTypes.string 7 | componentDidMount: () -> 8 | Mousetrap.bind @props.shortcut, @props.onClick 9 | componentWillUnmount: () -> 10 | Mousetrap.unbind @props.shortcut 11 | render: () -> 12 | 15 | -------------------------------------------------------------------------------- /app/styles/_colors.scss: -------------------------------------------------------------------------------- 1 | //Color names 2 | $near-black: rgb(48,58,70); 3 | $white: rgb(250,250,250); 4 | $light-gray: rgb(229, 230, 226); 5 | $gray: rgb(204,204,204); //#CCCCCC 6 | $dark-gray: rgb(154,154,154); 7 | $muted-gray: rgb(104,127,143); 8 | $blue: rgb(32,130,166); 9 | $red: rgb(245,0,49); 10 | $pink: rgb(247, 161, 196); 11 | $alert-yellow: rgb(238,245,77); 12 | $dark-red: rgb(123, 45, 47); 13 | $green: rgb( 96,176,68); 14 | //Purpose names 15 | $inactive-color: $gray; 16 | $alert-color: $alert-yellow; 17 | -------------------------------------------------------------------------------- /app/scripts/models/__mocks__/pass.coffee: -------------------------------------------------------------------------------- 1 | Promise = require.requireActual 'bluebird' 2 | passMock = jest.genMockFromModule('../pass') 3 | passMock.find.mockImplementation (id) -> 4 | Promise.resolve(if typeof id is 'object' then id else null) 5 | passMock.findBy.mockReturnValue(Promise.resolve([])) 6 | passMock.findByOrCreate.mockImplementation (query, opts) -> 7 | Promise.resolve(new passMock(opts) for opt in opts) 8 | passMock.findOrCreate.mockImplementation (opts) -> 9 | Promise.resolve(new passMock(opts)) 10 | module.exports = passMock -------------------------------------------------------------------------------- /app/scripts/models/game_metadata.coffee: -------------------------------------------------------------------------------- 1 | AppDispatcher = require '../dispatcher/app_dispatcher' 2 | {ActionTypes} = require '../constants' 3 | Store = require './store' 4 | class GameMetadata extends Store 5 | @dispatchToken: AppDispatcher.register (action) => 6 | switch action.type 7 | when ActionTypes.SYNC_GAMES 8 | new GameMetadata(obj).save() for obj in action.games 9 | @emitChange() 10 | constructor: (options={}) -> 11 | super options 12 | @display = options.display ? 'Untitled Game' 13 | module.exports = GameMetadata 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/scripts/models/__mocks__/box_entry.coffee: -------------------------------------------------------------------------------- 1 | Promise = require.requireActual('bluebird') 2 | boxMock = jest.genMockFromModule('../box_entry') 3 | boxMock.find.mockImplementation (id) -> 4 | Promise.resolve id: id 5 | boxMock.findBy.mockReturnValue (Promise.resolve([])) 6 | boxMock.findByOrCreate.mockImplementation (query, opts) -> 7 | Promise.resolve(new boxMock(opts) for opt in opts) 8 | boxMock.findOrCreate.mockImplementation (opts) -> 9 | Promise.resolve(new boxMock(opts)) 10 | boxMock.new.mockImplementation (opts) -> 11 | Promise.resolve(new boxMock(opts)) 12 | module.exports = boxMock -------------------------------------------------------------------------------- /app/scripts/models/__mocks__/jam.coffee: -------------------------------------------------------------------------------- 1 | Promise = require.requireActual('bluebird') 2 | jamMock = jest.genMockFromModule('../jam') 3 | jamMock.find.mockImplementation (id) -> 4 | Promise.resolve(if typeof id is 'object' then id else null) 5 | jamMock.findBy.mockReturnValue(Promise.resolve([])) 6 | jamMock.findByOrCreate.mockImplementation (query, opts) -> 7 | Promise.resolve(new jamMock(opts) for opt in opts) 8 | jamMock.findOrCreate.mockImplementation (opts) -> 9 | Promise.resolve(new jamMock(opts)) 10 | jamMock.prototype.getPositionsInBox.mockReturnValue([]) 11 | module.exports = jamMock -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | In order to be the most helpful please check that you have the following information before proceeding: 2 | 3 | - [ ] Describe the environment you are encountering the issue in 4 | - Operating System 5 | - Browsers Used 6 | - BoutTime Version `bouttime-server --version` or `./bin/bouttime-server --version` 7 | - Node Version `node --version` 8 | 9 | - [ ] Please provide detailed steps necessary to reproduce your issue 10 | 11 | - [ ] Please include any screenshots as needed 12 | 13 | And as always, **please** check to see if your issue has already been reported! 14 | -------------------------------------------------------------------------------- /app/images/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/scripts/models/__mocks__/skater.coffee: -------------------------------------------------------------------------------- 1 | Promise = require.requireActual('bluebird') 2 | skaterMock = jest.genMockFromModule('../skater') 3 | skaterMock.find.mockImplementation (id) -> 4 | Promise.resolve id: id 5 | skaterMock.findBy.mockReturnValue (Promise.resolve([])) 6 | skaterMock.findByOrCreate.mockImplementation (query, opts) -> 7 | Promise.resolve(new skaterMock(opts) for opt in opts) 8 | skaterMock.findOrCreate.mockImplementation (opts) -> 9 | Promise.resolve(new skaterMock(opts)) 10 | skaterMock.new.mockImplementation (opts) -> 11 | Promise.resolve(new skaterMock(opts)) 12 | module.exports = skaterMock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Women's Flat Track Derby Association 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /app/styles/_item-row.scss: -------------------------------------------------------------------------------- 1 | .item-row{ 2 | .drag-handle { 3 | cursor: move; 4 | } 5 | .options-wrapper { 6 | position: absolute; 7 | overflow: hidden; 8 | visibility: hidden; 9 | .options { 10 | padding: 0; 11 | transform: translateX(-100%); 12 | transition: transform 0.5s, visibility 0.0s 0.5s; 13 | .btn { 14 | padding: 8px; 15 | } 16 | } 17 | } 18 | &.opened { 19 | .options-wrapper { 20 | visibility: visible; 21 | .options { 22 | transform: translateX(0); 23 | transition: transform 0.5s, visibility 0.0s 0.0s; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/styles/_penalty-box-timer.scss: -------------------------------------------------------------------------------- 1 | .penalty-box-timer { 2 | .penalty-clock { 3 | .penalty-count { 4 | position: absolute; 5 | height: 2em; 6 | width: 2em; 7 | right: 2px; 8 | top: 0px; 9 | text-align: center; 10 | padding-top: 0.25em; 11 | } 12 | .clock { 13 | padding: 1px; 14 | font-size: 3.7em; 15 | } 16 | } 17 | .period-clock { 18 | background-color: $white; 19 | font-weight: lighter; 20 | margin-bottom: 10px; 21 | } 22 | .jam-clock { 23 | border: none; 24 | font-weight: bold; 25 | background-color: $light-gray; 26 | color: $near-black; 27 | margin-bottom: 10px; 28 | } 29 | } -------------------------------------------------------------------------------- /app/scripts/components/penalty_whiteboard.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react/addons' 2 | TeamSelector = require './shared/team_selector.cjsx' 3 | PenaltiesSummary = require './penalty_tracker/penalties_summary.cjsx' 4 | cx = React.addons.classSet 5 | module.exports = React.createClass 6 | displayName: 'PenaltyWhiteBoard' 7 | render: () -> 8 | awayElement = 9 | homeElement = 10 |
11 | 16 |
17 | -------------------------------------------------------------------------------- /app/scripts/components/scorekeeper/score_note.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react/addons' 2 | cx = React.addons.classSet 3 | module.exports = React.createClass 4 | displayName: 'ScoreNote' 5 | propTypes: 6 | note: React.PropTypes.string 7 | noteContent: () -> 8 | switch @props.note 9 | when 'injury' then 'Injury' 10 | when 'calloff' then 'Call' 11 | when 'nopass' then 'No P.' 12 | when 'lead' then 'Lead' 13 | when 'lost' then 'Lost' 14 | else   15 | render: () -> 16 | noteClass = cx 17 | 'box-primary': @props.note? 18 | 'bt-box box-default text-center': true 19 |
20 | {@noteContent()} 21 |
22 | -------------------------------------------------------------------------------- /app/scripts/functions.coffee: -------------------------------------------------------------------------------- 1 | constants = require './constants' 2 | module.exports = 3 | #Creates a pseudo unique Id 4 | uniqueId: (length=8, rng=Math.random) -> 5 | id = "" 6 | id += rng().toString(36).substr(2) while id.length < length 7 | id.substr 0, length 8 | #Pads a number 9 | pad: (num, digits) -> 10 | if num.toString().length < digits 11 | ("000" + num).substr(-digits) 12 | else 13 | num 14 | #Gets a URL Parameter non-obtusely 15 | getParams: -> 16 | query = window.location.search.substring(1) 17 | raw_vars = query.split("&") 18 | params = {} 19 | for v in raw_vars 20 | [key, val] = v.split("=") 21 | params[key] = decodeURIComponent(val) 22 | params 23 | -------------------------------------------------------------------------------- /app/styles/mixins/_border-arrow.scss: -------------------------------------------------------------------------------- 1 | @mixin border-arrow($left) { 2 | background-color: $muted-gray; 3 | padding: 14px; 4 | margin-top: 18px; 5 | position: relative; 6 | &:after, &:before { 7 | bottom: 100%; 8 | left: $left; 9 | border: solid transparent; 10 | content: " "; 11 | height: 0; 12 | width: 0; 13 | position: absolute; 14 | pointer-events: none; 15 | } 16 | &:after { 17 | border-color: rgba(0, 0, 0, 0); 18 | border-bottom-color: $muted-gray; 19 | border-width: 10px; 20 | margin-left: -10px; 21 | } 22 | &:before { 23 | border-color: rgba(0, 0, 0, 0); 24 | border-bottom-color: $muted-gray; 25 | border-width: 16px; 26 | margin-left: -16px; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/scripts/components/lineup_tracker/team_lineup.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react/addons' 2 | JamDetail = require './jam_detail.cjsx' 3 | LineupTrackerActions = require './lineup_tracker_actions.cjsx' 4 | cx = React.addons.classSet 5 | module.exports = React.createClass 6 | displayName: 'TeamLineup' 7 | propTypes: 8 | team: React.PropTypes.object.isRequired 9 | setSelectorContextHandler: React.PropTypes.func.isRequired 10 | render: ()-> 11 |
12 | {@props.team.jams.map (jam, jamIndex) -> 13 | 18 | , this } 19 | 20 |
-------------------------------------------------------------------------------- /app/scripts/components/penalty_tracker/penalty_alert.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react/addons' 2 | cx = React.addons.classSet 3 | module.exports = React.createClass 4 | displayName: 'PenaltyAlert' 5 | propTypes: 6 | skater: React.PropTypes.object.isRequired 7 | render: () -> 8 | containerClass = cx 9 | 'penalty-alert text-center text-uppercase bt-box': true 10 | 'box-warning': @props.skater.leftEarly() 11 | 'box-danger': @props.skater.fouledOut() or @props.skater.expelled() 12 | displayContent = switch 13 | when @props.skater.expelled() then 'Expelled' 14 | when @props.skater.fouledOut() then 'Foul Out' 15 | when @props.skater.leftEarly() then 'Left Early' 16 | else 'Alert' 17 |
18 | {displayContent} 19 |
-------------------------------------------------------------------------------- /app/scripts/shallowCompare.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 shallowCompare 10 | */ 11 | 12 | 'use strict'; 13 | 14 | var shallowEqual = require('shallowEqual'); 15 | 16 | /** 17 | * Does a shallow comparison for props and state. 18 | * See ReactComponentWithPureRenderMixin 19 | */ 20 | function shallowCompare(instance, nextProps, nextState) { 21 | return ( 22 | !shallowEqual(instance.props, nextProps) || 23 | !shallowEqual(instance.state, nextState) 24 | ); 25 | } 26 | 27 | module.exports = shallowCompare; 28 | -------------------------------------------------------------------------------- /app/scripts/components/jam_timer/timeout_bars.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react/addons' 2 | module.exports = React.createClass 3 | displayName: 'TimeoutBars' 4 | shouldComponentUpdate: (nprops, nstate) -> 5 | _.isEqual(@props, nprops) == false 6 | render: () -> 7 |
8 | {@props.initials} 9 |
10 |
11 |
12 |
13 |
-------------------------------------------------------------------------------- /app/scripts/components/penalty_tracker.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react/addons' 2 | functions = require '../functions.coffee' 3 | TeamSelector = require './shared/team_selector.cjsx' 4 | TeamPenalties = require './penalty_tracker/team_penalties.cjsx' 5 | Skater = require '../models/skater.coffee' 6 | cx = React.addons.classSet 7 | module.exports = React.createClass 8 | displayName: 'PenaltyTracker' 9 | render: () -> 10 | awayElement = 11 | homeElement = 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 |
24 |
25 |
{text}
26 |
27 |
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 |
12 | 13 | menu 14 | 15 | 29 |
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 |
22 |
23 |
24 | 28 |
29 |
30 | 33 |
34 |
35 | 40 |
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 |
37 | 41 |
42 | , this} 43 |
44 | 45 |
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 |
37 |
38 |
39 |
40 |
41 |
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 | 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 | [![CircleCI](https://circleci.com/gh/WFTDA/bouttime.svg?style=svg&circle-token=9b5d2312f6063d633b844c97c873653c13b26513)](https://circleci.com/gh/WFTDA/bouttime) 2 | [![Code Climate](https://codeclimate.com/github/DerbyBoutTime/bouttime/badges/gpa.svg)](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 |
-------------------------------------------------------------------------------- /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 |
57 |
58 | 65 |
66 |
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 |
22 |
23 |
24 |
25 | 75 |
76 |
77 |
78 |
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 |
40 |
41 |

Game Setup

42 |
43 | 44 | 45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 |
53 | 54 | 55 | 56 | 57 |
58 |
59 |
60 | 61 |
62 | 63 | 64 | 65 | 66 |
67 |
68 | {@props.gameState.ads.map (ad, idx) -> 69 |
70 | 71 | 72 |
73 | , this} 74 |
75 | 76 | 77 |
78 |

Officials

79 | {@props.gameState.officials.map (official, idx) -> 80 |
81 | 82 |
83 | 84 | 85 |
86 |
87 | , this} 88 | 89 |
90 | 91 | 92 |
93 | 94 |
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) --------------------------------------------------------------------------------