├── .firebaserc ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmrc ├── .project ├── .travis.yml ├── LICENSE.md ├── Makefile ├── Procfile ├── README.md ├── checklist.md ├── database.json ├── documentjs.json ├── firebase.json ├── index.js ├── install.js ├── migrations ├── 20150801045523-players.js ├── 20150804053921-add-stats.js ├── 20150809023413-add-stats-time.js ├── 20150816063154-add-users.js ├── 20160301185116-required-fields.js ├── 20160307152540-validate-emails.js └── 20160313205522-team-ints.js ├── models ├── bookshelf.js ├── game.js ├── player.js ├── stat.js ├── team.js ├── tournament.js └── user.js ├── package.json ├── public ├── .npmrc ├── app.js ├── app.less ├── build.js ├── components │ ├── 404.component │ ├── game │ │ └── details │ │ │ ├── details.html │ │ │ ├── details.js │ │ │ ├── details.less │ │ │ ├── details.md │ │ │ ├── details.stache │ │ │ ├── details_test.js │ │ │ └── test.html │ ├── navigation │ │ ├── img │ │ │ └── bitballs-logo-01.svg │ │ ├── navigation.html │ │ ├── navigation.js │ │ ├── navigation.less │ │ ├── navigation.stache │ │ ├── navigation_test.js │ │ └── test.html │ ├── player │ │ ├── details │ │ │ ├── details-test.js │ │ │ ├── details.html │ │ │ ├── details.js │ │ │ ├── details.less │ │ │ ├── details.md │ │ │ ├── details.stache │ │ │ └── test.html │ │ ├── edit │ │ │ ├── edit.html │ │ │ ├── edit.js │ │ │ ├── edit.md │ │ │ ├── edit.stache │ │ │ ├── edit_test.js │ │ │ └── test.html │ │ └── list │ │ │ ├── list.html │ │ │ ├── list.js │ │ │ ├── list.stache │ │ │ ├── list_test.js │ │ │ └── test.html │ ├── test.js │ ├── tournament │ │ ├── details │ │ │ ├── details.html │ │ │ ├── details.js │ │ │ ├── details.stache │ │ │ ├── details_test.js │ │ │ └── test.html │ │ └── list │ │ │ ├── list.html │ │ │ ├── list.js │ │ │ ├── list.stache │ │ │ ├── list_test.js │ │ │ └── test.html │ └── user │ │ ├── details │ │ ├── details.html │ │ ├── details.js │ │ ├── details.stache │ │ ├── details_test.js │ │ └── test.html │ │ ├── list │ │ ├── list.html │ │ ├── list.js │ │ ├── list.less │ │ ├── list.md │ │ ├── list.stache │ │ ├── list_test.js │ │ └── test.html │ │ └── test.js ├── dev.html ├── img │ └── bitballs-logo-02.svg ├── index.stache ├── inserted-removed.js ├── is-dev.js ├── models │ ├── bookshelf-service.js │ ├── fixtures │ │ ├── fixtures.js │ │ ├── games.js │ │ ├── players.js │ │ ├── stats.js │ │ ├── teams.js │ │ ├── tournaments.js │ │ └── users.js │ ├── game.js │ ├── game_test.js │ ├── player.js │ ├── session.js │ ├── stat.js │ ├── stat_test.js │ ├── team.js │ ├── test.html │ ├── test.js │ ├── tournament.js │ ├── tournament_test.js │ ├── user.js │ └── youtube.js ├── package.json ├── prod.html ├── service.js ├── test.html ├── test.js ├── test │ └── utils.js └── util │ ├── prefilter.js │ ├── test.html │ └── test.js ├── services ├── adminOnly.js ├── app.js ├── email.js ├── games.js ├── players.js ├── separate-query.js ├── session.js ├── stats.js ├── teams.js ├── tournaments.js └── users.js ├── test-script.md └── theme ├── static ├── content_list.js └── static.js └── templates └── helpers.js /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "bitballs-e69ca" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | package-lock.json 9 | node_modules 10 | .env 11 | public/node_modules 12 | public/dist 13 | public/package-lock.json 14 | docs/ 15 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | public/node_modules/** 3 | public/dist/** 4 | docs/** 5 | theme/static/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "it": true, 4 | "describe": true, 5 | "before": true, 6 | "beforeEach": true, 7 | "after": true, 8 | "afterEach": true, 9 | "exports": true, 10 | "bundle": true, 11 | "doneSsr": true, 12 | "Promise": true, 13 | "INLINE_CACHE": true, 14 | "canWait": true, 15 | "confirm": true, 16 | "steal": true, 17 | "YT": true 18 | }, 19 | "curly": true, 20 | "eqeqeq": true, 21 | "freeze": true, 22 | "indent": 2, 23 | "latedef": false, 24 | "noarg": true, 25 | "undef": true, 26 | "unused": "vars", 27 | "trailing": true, 28 | "maxdepth": 4, 29 | "boss" : true, 30 | "eqnull": true, 31 | "evil": true, 32 | "loopfunc": true, 33 | "smarttabs": true, 34 | "maxerr" : 200, 35 | "browser": true, 36 | "phantom": true, 37 | "node": true, 38 | "esversion": 6 39 | } 40 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | bitballs 4 | 5 | 6 | 7 | 8 | 9 | com.aptana.ide.core.unifiedBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.aptana.ruby.core.rubynature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: '10' 3 | sudo: required 4 | dist: trusty 5 | addons: 6 | firefox: 52.3.0 7 | postgresql: '9.4' 8 | apt: 9 | packages: 10 | - dbus-x11 11 | install: 12 | - npm install 13 | before_install: 14 | - export DISPLAY=:99.0 15 | - sh -e /etc/init.d/xvfb start 16 | - psql -c 'create database bitballs;' -U postgres 17 | before_deploy: 18 | - git config --global user.email "justin@bitovi.com" 19 | - git config --global user.name "bitballs deploy bot" 20 | - node public/build 21 | - git add public/dist/ --force 22 | - git commit -m "Updating build." 23 | - npm run deploy:ci 24 | deploy: 25 | skip_cleanup: true 26 | provider: heroku 27 | app: bitballs 28 | api_key: 29 | secure: XMm4TV8T+r2XFWdxJfQdPELKl0z08oyQLzkLgb13G3PWMqrtyF4u2Yx/c3Xxv2aT7HGOmsO1HZ4OLGvywmhIRgKbSIJpks/UU0DhH+gwtw1Y7LYCmbo2UCVTvle45Vg6M0LUWXS8ncWjHUr5ZyjbqHqpVRVqvebWBvD6f5NteZf2arKvJVvKzifSReAGfc/L22V3aCLYW57jKeo9RFG2SYGgCxn+Lqf2CEyxm0RARwOKNShqkzRvugvAJVCz2vkEEE8pvLBkc5KpjSqF60NbQGrXZvFELkk8OTx/7pc6DI3N9j4tcifkfzl8+AxXPpwyA/i9Jx6233STI44Y4JkwARYx/r7ylMj/s0f7tcO7RZVtga9zzMM5KmpX97nbXOYd6rS3qnKthoS/N/XXRFpTUxe4QYFtrjoQPo5HdUzaLarI0ehHFFJLhlUr55KPlufKcIjbD5a2aSkVIeb0sfN6uiQtKuz8c4kbZS+uGYd0F7B4pdxnPV1bh2FiuCUE6o44Ny2FVUb/BZSrtAVcO+jqhCaCb1JUTLPEhxV1UHIVsD1gSJAng23GphtHjdlh2uV2Vcy58Gx0BGkPHe25ypT5HsGnFQvECaT0fBd13thqr0MaBYxinRCUhF79crldd3D7LVT6RRENUFSQ2hpO0YqW028vUT+jm3DIugP5vEbnhkk= 30 | env: 31 | global: 32 | secure: GtZ4Jqwgp5V9n+IC51FZT5KjIHMwmBXXntAyXCEQZopX0pGaya6IsJLvbQhkjFhBzJFnLVVkLbTpPaax8Gpf2a7Pwa/4ev8X7n1KxgTeupdpX7Ur8IO6np45n0o2BmE3l7+YOEzt6hC+rxYWPdcsVABTf8No5pPOVFgBL3GfM/fc2evn1wqJ5lGyPvQPJvJiRokznKxxnpqQ2W6C8P+LQy5v3iJJTqnNKWzJOj7dOPznOSdHA3sSJM+u/OjpyrPITsuXGTLXycMr/LK5jpyKUBIrFRZq7aY40+8hX8ZY+dMBHEuh7nIPHd45jqZx3Xk9Vgl974bWk1YK7ZqKk0HaveReqtprcKmEbc6toG22TFSyp3lcXkhLQV+wRWw0yrJ/czGbkuZU4jcO6r4ge75Bfi9+tUhqvMxk1sETkOunYoFpbyiUa7YU/ucH+hCcofphRH6NEtKdus749VdvcPu45rnL/zPxSs+5l8rpwWTp6XpcN0w8+MDYDhMSs6YPs9ltr0fP4U9amRjORU3PGkq7McAaXnenv3r2K5HSG8rFWTV5Te4ckj6MOIiU+tDSx0imClVccMr73NMJV0BVbkOWhef0uDlMb3uFFsPBj/t4OH1UkuhT+UeCNFCHxflj9aJEJe56NLD0ojZFeA1del7Cdl8yqWHL3b2sAkeRyWG09yM= 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Bitovi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | publish-docs: 2 | git checkout -b gh-pages 3 | donejs document 4 | git add -f docs/ 5 | git add -f public/components/ 6 | git add -f public/models/ 7 | git add -f public/node_modules/jquery 8 | git add -f public/node_modules/can 9 | git add -f public/node_modules/can-connect 10 | git add -f public/node_modules/can-fixture 11 | git add -f public/node_modules/can-set 12 | git add -f public/node_modules/can-zone 13 | git add -f public/node_modules/bootstrap 14 | git add -f public/node_modules/done-autorender 15 | git add -f public/node_modules/done-component 16 | git add -f public/node_modules/done-css 17 | git add -f public/node_modules/done-ssr-middleware 18 | git add -f public/node_modules/donejs-cli 19 | git add -f public/node_modules/funcunit 20 | git add -f public/node_modules/generator-donejs 21 | git add -f public/node_modules/moment 22 | git add -f public/node_modules/qunitjs 23 | git add -f public/node_modules/steal 24 | git add -f public/node_modules/steal-qunit 25 | git add -f public/node_modules/steal-systemjs 26 | git add -f public/node_modules/steal-es6-module-loader 27 | git add -f public/node_modules/steal-platform 28 | git add -f public/node_modules/when 29 | git add -f public/node_modules/yeoman-environment 30 | git add -f public/test.html 31 | git commit -m "Publish docs" 32 | git push -f origin gh-pages 33 | git rm -q -r --cached public/node_modules 34 | git checkout - 35 | git branch -D gh-pages -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | @page bitballs Bitballs 2 | @group bitballs.components Components 3 | @group bitballs.clientModels Client Models 4 | @group bitballs.services Services 5 | @group bitballs.serviceModels Service Models 6 | @hide contents 7 | 8 | [![Build Status](https://travis-ci.org/donejs/bitballs.svg?branch=master)](https://travis-ci.org/donejs/bitballs) 9 | 10 | Bitballs is a [DoneJS](https://donejs.com) app that enables users to coordinate 11 | the players, teams, games, rounds and recordings of a basketball tournament. 12 | It also serves as an example of how to use DoneJS with sessions, user 13 | privileges, RESTful services, and ORM models. 14 | 15 | To run the Bitballs app locally, run its tests, or generate its documentation 16 | follow the steps outlined below. 17 | 18 | 19 | 20 | 21 | 22 | - [Setup Environment](#setup-environment) 23 | - [Installing PostgreSQL on OSX](#installing-postgresql-on-osx) 24 | - [Installing PostgreSQL on Linux](#installing-postgresql-on-linux) 25 | - [Installing PostgreSQL on Windows](#installing-postgresql-on-windows) 26 | - [Download Source](#download-source) 27 | - [Install Dependencies](#install-dependencies) 28 | - [Prepare the Database](#prepare-the-database) 29 | - [Start the Server](#start-the-server) 30 | - [Register a User](#register-a-user) 31 | - [Enjoy](#enjoy) 32 | 33 | 34 | 35 | ### Setup Environment 36 | 37 | Make sure you have installed: 38 | 39 | - [Node 5](https://nodejs.org/en/download/) 40 | - NPM 3 *(packaged with Node)* 41 | - [PostgreSQL](https://www.postgresql.org/download/) 42 | 43 | #### Installing PostgreSQL on OSX 44 | 45 | On a Mac, the easiest way to install and configure [PostgreSQL](https://www.postgresql.org) 46 | is using the [brew](https://brew.sh/) utility: 47 | 48 | ``` 49 | brew install postgresql 50 | ``` 51 | 52 | Pay special attention to the end of the [brew](https://brew.sh/) command's 53 | output, which includes instructions on how to start `postgres`: 54 | 55 | ``` 56 | To load postgresql: 57 | launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist 58 | Or, if you don't want/need launchctl, you can just run: 59 | postgres -D /usr/local/var/postgres 60 | ``` 61 | 62 | The provided `launchctl` command ensures the `postgres` process is always 63 | running, even after a system restart. The alternative `postgres` command 64 | starts the `postgres` process manually. 65 | 66 | We recommend the `launchctl` option. If desired, `postgres` can be 67 | stopped and uninstalled by running: 68 | 69 | ``` 70 | brew uninstall postgresql 71 | ``` 72 | 73 | #### Installing PostgreSQL on Linux 74 | 75 | *Coming Soon* 76 | 77 | #### Installing PostgreSQL on Windows 78 | 79 | Download and use the graphical installer available on [postgresql.org](http://www.postgresql.org/download/windows/). Make sure you host it listen to port `5432`. 80 | 81 | Open `pg_hba.conf`, which should be in _C:\Program Files\PostgreSQL\9.5\data_, and change from `md5` authentication to `trust`. For example, change: 82 | 83 | > host all all 127.0.0.1/32 md5 84 | 85 | to: 86 | 87 | > host all all 127.0.0.1/32 trust 88 | 89 | `trust` should not be used in a production environment. We are only using it here as a substitute for the `peer` mode available in UNIX environments. Read more about it [here](http://www.postgresql.org/docs/9.5/static/auth-methods.html). 90 | 91 | 92 | 93 | Finally, using `pgAdmin III` graphical database manager, which should have been installed with `postgres`, create a `bitballs` database. 94 | 95 | 96 | ### Download Source 97 | 98 | Clone this repo using git: 99 | 100 | ``` 101 | git clone https://github.com/donejs/bitballs.git 102 | ``` 103 | 104 | Navigate to the repository's directory 105 | 106 | ``` 107 | cd bitballs 108 | ``` 109 | 110 | ### Prepare the Database 111 | 112 | Make sure the `postgres` process is running: 113 | 114 | ``` 115 | ps | grep postgres 116 | ``` 117 | 118 | You should see "postgres -D" among the output: 119 | 120 | ``` 121 | 92831 ttys000 0:00.02 postgres -D /usr/local/var/postgres 122 | 92856 ttys000 0:00.00 grep postgres 123 | ``` 124 | 125 | With that confirmed we can create the database that the bitballs app 126 | will persist its data to: 127 | 128 | ``` 129 | createdb bitballs 130 | ``` 131 | 132 | ### Install Dependencies 133 | 134 | To install the project's JavaScript dependencies run: 135 | 136 | ``` 137 | npm install 138 | ``` 139 | 140 | Additionally DoneJS's command line utilities need to be installed globally: 141 | 142 | ``` 143 | npm install -g donejs-cli 144 | ``` 145 | 146 | ### Start the Server 147 | 148 | With all the prerequisite setup completed the server can be started by running: 149 | 150 | ``` 151 | donejs develop 152 | ``` 153 | 154 | ### Register a User 155 | 156 | Navigate to [http://localhost:5000/register](http://localhost:5000/register) 157 | in your browser and follow the instructions. 158 | 159 | ### Enjoy 160 | 161 | You're finished! Explore some of the app's features: 162 | 163 | - Live reload (`donejs develop`) 164 | - Run the tests (`donejs test`) 165 | - Generate the documentation (`donejs document`) 166 | -------------------------------------------------------------------------------- /checklist.md: -------------------------------------------------------------------------------- 1 | ## What is the project's vision? 2 | 3 | > This is typically a single sentence that describes what the project aspires to be. 4 | > Example: "A JS framework that allows developers to build better apps, faster". 5 | > If this doesn't exist, write "none". 6 | 7 | A guide and application that: 8 | 9 | - Teaches people how to solve common problems with DoneJS 10 | - Demonstrates that DoneJS elegantly solves common problems. 11 | 12 | Produce people who can build a good guide without much 13 | oversite. 14 | 15 | ## How will the project measure success? 16 | 17 | > Example: Increase mobile conversion rates to 0.75-1.0%, currently ~0.3%. If this doesn't exist, write "none". 18 | 19 | - Approximately as many page views as PMO. 20 | - If a question is directly related to parts of this guide, 21 | we should be able to post a link to the relavant section and there are no follow ups except for 22 | ones not covered by the guide. Basically, if someone asks, how to deal with cookie based sessions, 23 | we should post to this guide's `session` section. 24 | - Github Stars? 25 | - Roomplanner and Jobcostracker are successful without Justin managing them (but he's involved when appropriate). 26 | 27 | 28 | ## What is the strategy for accomplishing the project's goals? 29 | 30 | > What is the strategy for accomplishing the project's goals? 31 | 32 | - Establish clear expectations. 33 | - Goal oriented 34 | - Start and end of day status updates. 35 | - Getting help is the solution to delivery quality wtihout working longer hours. 36 | - Manage by over-async-communicating tasks until developers can take over. 37 | - Be proactive if you don't have an issue. Upward manage expectations. 38 | 39 | - Sharing knowledge 40 | - Daily code reviews of previous day's pull requests or progress. 41 | - Commit to your branch every night. 42 | - Pairing? 43 | 44 | ## What is the project's roadmap? What are the goals, plans and release schedule after the current release? 45 | 46 | - [ ] - Identify common problems in Bitballs that are not in 47 | PMO and DoneJS Chat. 48 | 49 | - Sessions, simple admin rights. 50 | - Relationships between models + derived values. 51 | - Node restful services and server-side rendering mixed. 52 | - Ignore parts of SSR 53 | 54 | - [ ] - Outline the guide. 55 | 56 | - [ ] - Get DoneJS essential features in place. 57 | 58 | - [ ] - Justin write the guide. 59 | 60 | - [ ] - Convert to a "good" DoneJS app 61 | - tests 62 | - docs 63 | - demo pages 64 | 65 | - Put it together. 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /database.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": "postgres://localhost:5432/bitballs", 3 | "prod": { 4 | "ENV": "DATABASE_URL", 5 | "driver": "pg" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /documentjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "sites" : { 3 | "docs": { 4 | "glob" : { 5 | "ignore": ["{node_modules,public/node_modules,migrations,public/dist}/**/*"] 6 | }, 7 | "parent": "bitballs", 8 | "dest": "docs" 9 | } 10 | }, 11 | "siteDefaults": { 12 | "static": "theme/static", 13 | "source": "https://github.com/donejs/bitballs", 14 | "templates" : "theme/templates" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "firebase": "bitballs-e69ca", 4 | "public": "./public/dist", 5 | "headers": [ 6 | { 7 | "source": "/**", 8 | "headers": [ 9 | { 10 | "key": "Access-Control-Allow-Origin", 11 | "value": "*" 12 | } 13 | ] 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = require('./services/app'); 3 | var exec = require( "child_process" ).exec; 4 | var cookieParser = require('cookie-parser'); 5 | var path = require("path"); 6 | 7 | app.set('port', (process.env.PORT || 5000)); 8 | 9 | app.use( express.static(__dirname + '/public') ); 10 | 11 | app.use(cookieParser()); 12 | 13 | if ( process.argv.indexOf( "--slow" ) !== -1 ) { 14 | console.log("Delaying everything 1 second"); 15 | app.use( function ( req, res, next ) { 16 | setTimeout(next, 1000); 17 | }); 18 | } 19 | 20 | require('./services/session'); 21 | 22 | require('./services/games'); 23 | require('./services/players'); 24 | require('./services/stats'); 25 | require('./services/teams'); 26 | require('./services/tournaments'); 27 | require('./services/users'); 28 | 29 | //can-ssr: 30 | app.use( "/", require('./public/service') ); 31 | 32 | app.listen(app.get('port'), function() { 33 | console.log('Node app is running on port', app.get('port')); 34 | }); 35 | 36 | if ( process.argv.indexOf( "--develop" ) !== -1 ) { 37 | //is dev mode so do live reload 38 | var child = exec( path.join("node_modules",".bin","steal-tools live-reload"), { 39 | cwd: process.cwd() + "/public" 40 | }); 41 | 42 | child.stdout.pipe( process.stdout ); 43 | child.stderr.pipe( process.stderr ); 44 | } 45 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | var exec = require( "child_process" ).exec; 2 | 3 | var child = exec( "npm install", { 4 | cwd: process.cwd() + "/public" 5 | }); 6 | 7 | child.stdout.pipe( process.stdout ); 8 | child.stderr.pipe( process.stderr ); 9 | -------------------------------------------------------------------------------- /migrations/20150801045523-players.js: -------------------------------------------------------------------------------- 1 | exports.up = function(db, callback) { 2 | db.createTable('players', { 3 | id: { type: 'int', primaryKey: true, autoIncrement: true }, 4 | name: 'string', 5 | weight: 'int', 6 | height: 'int', 7 | birthday: 'date', 8 | profile: 'text', 9 | startRank: 'string' 10 | }, callback); 11 | }; 12 | 13 | exports.down = function(db, callback) { 14 | db.dropTable('players', callback); 15 | }; 16 | -------------------------------------------------------------------------------- /migrations/20150804053921-add-stats.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | 3 | exports.up = function(db, callback) { 4 | async.series([ 5 | db.createTable.bind(db, 'tournaments', { 6 | id: { type: 'int', primaryKey: true, autoIncrement: true }, 7 | date: 'date' 8 | }), 9 | db.createTable.bind(db, 'teams', { 10 | id: { type: 'int', primaryKey: true, autoIncrement: true }, 11 | tournamentId: 'int', 12 | name: 'string', 13 | color: 'string', 14 | player1Id: 'int', 15 | player2Id: 'int', 16 | player3Id: 'int', 17 | player4Id: 'int' 18 | }), 19 | db.createTable.bind(db, 'games', { 20 | id: { type: 'int', primaryKey: true, autoIncrement: true }, 21 | tournamentId: 'int', 22 | round: 'string', 23 | court: 'string', 24 | videoUrl: 'string', 25 | homeTeamId: 'string', 26 | awayTeamId: 'string' 27 | }), 28 | db.createTable.bind(db, 'stats', { 29 | id: { type: 'int', primaryKey: true, autoIncrement: true }, 30 | gameId: 'int', 31 | playerId: 'int', 32 | type: 'string' 33 | }) 34 | ], callback); 35 | }; 36 | 37 | exports.down = function(db, callback) { 38 | async.series([ 39 | db.dropTable.bind(db, 'tournaments'), 40 | db.dropTable.bind(db, 'teams'), 41 | db.dropTable.bind(db, 'games'), 42 | db.dropTable.bind(db, 'stats') 43 | ], callback); 44 | }; 45 | 46 | -------------------------------------------------------------------------------- /migrations/20150809023413-add-stats-time.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | 3 | exports.up = function(db, callback) { 4 | async.series([ 5 | db.addColumn.bind(db, "stats","time",{type: 'int'}), 6 | db.addColumn.bind(db, "stats","value",{type: 'int'}) 7 | ], callback); 8 | }; 9 | 10 | exports.down = function(db, callback) { 11 | async.series([ 12 | db.removeColumn.bind(db, "stats","time"), 13 | db.removeColumn.bind(db, "stats","value") 14 | ], callback); 15 | }; 16 | -------------------------------------------------------------------------------- /migrations/20150816063154-add-users.js: -------------------------------------------------------------------------------- 1 | exports.up = function(db, callback) { 2 | db.createTable('users', { 3 | id: { type: 'int', primaryKey: true, autoIncrement: true }, 4 | name: 'string', 5 | password: 'string', 6 | email: {type: 'string', unique: true}, 7 | isAdmin: 'boolean' 8 | }, callback); 9 | }; 10 | 11 | exports.down = function(db, callback) { 12 | db.dropTable('users', callback); 13 | }; 14 | -------------------------------------------------------------------------------- /migrations/20160301185116-required-fields.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | 3 | exports.up = function(db, callback) { 4 | async.series([ 5 | db.changeColumn.bind(db, 'players', 'name', { 6 | notNull: true 7 | }), 8 | db.changeColumn.bind(db, 'users', 'password', { 9 | notNull: true 10 | }), 11 | db.changeColumn.bind(db, 'tournaments', 'date', { 12 | notNull: true 13 | }) 14 | ], 15 | callback); 16 | }; 17 | 18 | exports.down = function(db, callback) { 19 | async.series([ 20 | db.changeColumn.bind(db, 'players', 'name', { 21 | notNull: false 22 | }), 23 | db.changeColumn.bind(db, 'users', 'password', { 24 | notNull: false 25 | }), 26 | db.changeColumn.bind(db, 'tournaments', 'date', { 27 | notNull: false 28 | }) 29 | ], 30 | callback); 31 | }; 32 | -------------------------------------------------------------------------------- /migrations/20160307152540-validate-emails.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | 3 | exports.up = function(db, callback) { 4 | async.series([ 5 | db.addColumn.bind(db, 'users', 'verified', { 6 | type: "boolean", 7 | notNull: true, 8 | defaultValue: false 9 | }), 10 | db.addColumn.bind(db, 'users', 'verificationHash', { 11 | type: "string", 12 | length: 100 13 | }) 14 | ], 15 | callback); 16 | }; 17 | 18 | exports.down = function(db, callback) { 19 | async.series([ 20 | db.removeColumn.bind(db, 'users', 'verified' ), 21 | db.removeColumn.bind(db, 'users', 'verificationHash' ) 22 | ], 23 | callback); 24 | }; 25 | -------------------------------------------------------------------------------- /migrations/20160313205522-team-ints.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | 3 | exports.up = function(db, callback) { 4 | async.series([ 5 | db.runSql.bind(db, 'ALTER TABLE games ALTER COLUMN "homeTeamId" TYPE integer USING "homeTeamId"::numeric'), 6 | db.runSql.bind(db, 'ALTER TABLE games ALTER COLUMN "awayTeamId" TYPE integer USING "awayTeamId"::numeric') 7 | ], 8 | callback); 9 | }; 10 | 11 | exports.down = function(db, callback) { 12 | async.series([ 13 | db.changeColumn.bind(db, 'games', 'homeTeamId', { 14 | type: 'string' 15 | }), 16 | db.changeColumn.bind(db, 'games', 'awayTeamId', { 17 | type: 'string' 18 | }) 19 | ], 20 | callback); 21 | }; 22 | -------------------------------------------------------------------------------- /models/bookshelf.js: -------------------------------------------------------------------------------- 1 | var dbConfig = require('../database.json'); 2 | var environmentKey = process.env.NODE_ENV === 'production' ? 'prod' : 'dev'; 3 | var dbEnvironmentConfig = dbConfig[environmentKey]; 4 | 5 | // Use the string itself or use the provided environment variable 6 | var connectionString = typeof dbEnvironmentConfig === 'string' ? 7 | dbEnvironmentConfig : 8 | process.env[dbEnvironmentConfig.ENV]; 9 | 10 | var knex = require('knex')({ 11 | client: 'pg', 12 | connection: connectionString 13 | }); 14 | 15 | module.exports = require('bookshelf')(knex); 16 | -------------------------------------------------------------------------------- /models/game.js: -------------------------------------------------------------------------------- 1 | var bookshelf = require("./bookshelf"); 2 | 3 | /** 4 | * @module {bookshelf.Model} models/game Game 5 | * @parent bitballs.serviceModels 6 | * 7 | * @group models/game.properties 0 properties 8 | * 9 | * @signature `new Game(properties)` 10 | * Creates an instance of a model. 11 | * 12 | * @param {Object} properties Initial values for this model's properties. 13 | */ 14 | 15 | var Game = bookshelf.Model.extend( 16 | /** @prototype **/ 17 | { 18 | /** 19 | * @property {String<"games">} models/game.properties.tableName tableName 20 | * @parent models/game.properties 21 | * 22 | * Indicates which database table Bookshelf.js will query against. 23 | **/ 24 | tableName: 'games', 25 | /** 26 | * @function 27 | * 28 | * Informs Bookshelf.js that the `stats` property will be a list of 29 | * [models/stat] models with a `gameId` that 30 | * matches the `id` specified in the query. 31 | **/ 32 | stats: function(){ 33 | var Stat = require("./stat"); 34 | return this.hasMany(Stat,"gameId"); 35 | }, 36 | /** 37 | * @function 38 | * 39 | * Informs Bookshelf.js that the `homeTeam` property will be a [models/team] 40 | * model with an `id` that matches the `homeTeamId` specified in the query. 41 | **/ 42 | homeTeam: function(){ 43 | var Team = require("./team"); 44 | return this.belongsTo(Team,"homeTeamId"); 45 | }, 46 | /** 47 | * @function 48 | * 49 | * Informs Bookshelf.js that the `awayTeam` property will be a [models/team] 50 | * model with an `id` that matches the `awayTeamId` specified in the query. 51 | **/ 52 | awayTeam: function(){ 53 | var Team = require("./team"); 54 | return this.belongsTo(Team,"awayTeamId"); 55 | }, 56 | /** 57 | * @function 58 | * 59 | * Informs Bookshelf.js that the `homeTeam` property will be a [models/tournament] 60 | * model with an `id` that matches the `tournamentId` specified in the query. 61 | **/ 62 | tournament: function(){ 63 | var Tournament = require("./tournament"); 64 | return this.belongsTo(Tournament, "tournamentId"); 65 | } 66 | }); 67 | 68 | module.exports = Game; 69 | -------------------------------------------------------------------------------- /models/player.js: -------------------------------------------------------------------------------- 1 | var bookshelf = require("./bookshelf"); 2 | var checkit = require("checkit"); 3 | 4 | /** 5 | * @module {bookshelf.Model} models/player Player 6 | * @parent bitballs.serviceModels 7 | * 8 | * @group models/player.properties 0 properties 9 | * 10 | * @signature `new Player(properties)` 11 | * Creates an instance of a model. 12 | * 13 | * @param {Object} properties Initial values for this model's properties. 14 | */ 15 | 16 | var Player = bookshelf.Model.extend( 17 | /** @prototype **/ 18 | { 19 | /** 20 | * @property {String<"players">} models/player.properties.tableName tableName 21 | * @parent models/player.properties 22 | * 23 | * Indicates which database table Bookshelf.js will query against. 24 | **/ 25 | tableName: 'players', 26 | /** 27 | * @function 28 | * 29 | * Binds to the "saving" event and specifies [models/player.prototype.validateSave validateSave] 30 | * as the handler during initialization. 31 | **/ 32 | initialize: function(){ 33 | this.on('saving', this.validateSave); 34 | }, 35 | /** 36 | * @function 37 | * 38 | * Validates that `name` is defined on `this.attributes`. 39 | * 40 | * @return {Promise} 41 | **/ 42 | validateSave: function(){ 43 | return checkit({ 44 | name: 'required' 45 | }).run(this.attributes); 46 | }, 47 | /** 48 | * @function 49 | * 50 | * Informs Bookshelf.js that the `stats` property will be a list of 51 | * [models/stat] models with a `playerId` that 52 | * matches the `id` specified in the query. 53 | **/ 54 | stats: function(){ 55 | var Stat = require("./stat"); 56 | return this.hasMany(Stat,"playerId"); 57 | }, 58 | games: function(){ 59 | var Game = require("./game"); 60 | return this.hasMany(Game,"playerId"); 61 | } 62 | }); 63 | 64 | module.exports = Player; 65 | -------------------------------------------------------------------------------- /models/stat.js: -------------------------------------------------------------------------------- 1 | var bookshelf = require("./bookshelf"); 2 | 3 | /** 4 | * @module {bookshelf.Model} models/stat Stat 5 | * @parent bitballs.serviceModels 6 | * 7 | * @group models/stat.properties 0 properties 8 | * 9 | * @signature `new Stat(properties)` 10 | * Creates an instance of a model. 11 | * 12 | * @param {Object} properties Initial values for this model's properties. 13 | */ 14 | 15 | var Stat = bookshelf.Model.extend( 16 | /** @prototype **/ 17 | { 18 | /** 19 | * @property {String<"stats">} models/stat.properties.tableName tableName 20 | * @parent models/stat.properties 21 | * 22 | * Indicates which database table Bookshelf.js will query against. 23 | **/ 24 | tableName: 'stats', 25 | /** 26 | * @function 27 | * 28 | * Informs Bookshelf.js that the `game` property will be a [models/game] 29 | * model with an `id` that matches the `gameId` specified in the query. 30 | **/ 31 | game: function(){ 32 | var Game = require("./game"); 33 | return this.belongsTo(Game,"gameId"); 34 | }, 35 | /** 36 | * @function 37 | * 38 | * Informs Bookshelf.js that the `player` property will be a [models/player] 39 | * model with an `id` that matches the `playerId` specified in the query. 40 | **/ 41 | player: function(){ 42 | var Player = require("./player"); 43 | return this.belongsTo(Player,"playerId"); 44 | } 45 | }); 46 | 47 | module.exports = Stat; 48 | -------------------------------------------------------------------------------- /models/team.js: -------------------------------------------------------------------------------- 1 | var bookshelf = require("./bookshelf"); 2 | var checkit = require("checkit"); 3 | 4 | /** 5 | * @module {bookshelf.Model} models/team Team 6 | * @parent bitballs.serviceModels 7 | * 8 | * @group models/team.properties 0 properties 9 | * 10 | * @signature `new Team(properties)` 11 | * Creates an instance of a model. 12 | * 13 | * @param {Object} properties Initial values for this model's properties. 14 | */ 15 | 16 | var Team = bookshelf.Model.extend( 17 | /** @prototype **/ 18 | { 19 | /** 20 | * @property {String<"teams">} models/team.properties.tableName tableName 21 | * @parent models/team.properties 22 | * 23 | * Indicates which database table Bookshelf.js will query against. 24 | **/ 25 | tableName: 'teams', 26 | initialize: function(){ 27 | this.on('saving', this.validateSave); 28 | }, 29 | /** 30 | * @function 31 | * 32 | * Validates fields and produces informative error messages 33 | * if the team can not be saved. 34 | */ 35 | validateSave: function(){ 36 | return checkit({ 37 | player1Id: {rule: 'required', message: 'Player 1 is required'}, 38 | player2Id: {rule: 'required', message: 'Player 2 is required'}, 39 | player3Id: {rule: 'required', message: 'Player 3 is required'}, 40 | player4Id: {rule: 'required', message: 'Player 4 is required'} 41 | }).run(this.attributes); 42 | }, 43 | /** 44 | * @function 45 | * 46 | * Informs Bookshelf.js that the `homeGames` property will be a list of 47 | * [models/game] models with a `homeTeamId` that 48 | * matches the `id` specified in the query. 49 | **/ 50 | homeGames: function(){ 51 | var Game = require("./game"); 52 | return this.hasMany(Game,"homeTeamId"); 53 | }, 54 | /** 55 | * @function 56 | * 57 | * Informs Bookshelf.js that the `awayGames` property will be a list of 58 | * [models/game] models with a `awayTeamId` that 59 | * matches the `id` specified in the query. 60 | **/ 61 | awayGames: function(){ 62 | var Game = require("./game"); 63 | return this.hasMany(Game,"awayTeamId"); 64 | }, 65 | /** 66 | * @function 67 | * 68 | * Informs Bookshelf.js that the `player1` property will be a [models/player] 69 | * model with an `id` that matches the `player1Id` specified in the query. 70 | **/ 71 | player1: function(){ 72 | var Player = require("./player"); 73 | return this.belongsTo(Player,"player1Id"); 74 | }, 75 | /** 76 | * @function 77 | * 78 | * Informs Bookshelf.js that the `player2` property will be a [models/player] 79 | * model with an `id` that matches the `player2Id` specified in the query. 80 | **/ 81 | player2: function(){ 82 | var Player = require("./player"); 83 | return this.belongsTo(Player,"player2Id"); 84 | }, 85 | /** 86 | * @function 87 | * 88 | * Informs Bookshelf.js that the `player3` property will be a [models/player] 89 | * model with an `id` that matches the `player3Id` specified in the query. 90 | **/ 91 | player3: function(){ 92 | var Player = require("./player"); 93 | return this.belongsTo(Player,"player3Id"); 94 | }, 95 | /** 96 | * @function 97 | * 98 | * Informs Bookshelf.js that the `player4` property will be a [models/player] 99 | * model with an `id` that matches the `player4Id` specified in the query. 100 | **/ 101 | player4: function(){ 102 | var Player = require("./player"); 103 | return this.belongsTo(Player,"player4Id"); 104 | } 105 | }); 106 | 107 | module.exports = Team; 108 | -------------------------------------------------------------------------------- /models/tournament.js: -------------------------------------------------------------------------------- 1 | var bookshelf = require("./bookshelf"); 2 | var checkit = require("checkit"); 3 | 4 | /** 5 | * @module {bookshelf.Model} models/tournament Tournament 6 | * @parent bitballs.serviceModels 7 | * 8 | * @group models/tournament.properties 0 properties 9 | * 10 | * @signature `new Tournament(properties)` 11 | * Creates an instance of a model. 12 | * 13 | * @param {Object} properties Initial values for this model's properties. 14 | */ 15 | 16 | var Tournament = bookshelf.Model.extend( 17 | /** @prototype **/ 18 | { 19 | /** 20 | * @property {String<"tournaments">} models/tournament.properties.tableName tableName 21 | * @parent models/tournament.properties 22 | * 23 | * Indicates which database table Bookshelf.js will query against. 24 | **/ 25 | tableName: 'tournaments', 26 | /** 27 | * @function 28 | * 29 | * Binds to the "saving" event and specifies [models/tournament.prototype.validateSave validateSave] 30 | * as the handler during initialization. 31 | **/ 32 | initialize: function(){ 33 | this.on('saving', this.validateSave); 34 | }, 35 | /** 36 | * @function 37 | * 38 | * Validates that `date` is defined on `this.attributes`. 39 | * 40 | * @return {Promise} 41 | **/ 42 | validateSave: function(){ 43 | return checkit({ 44 | date: ['required'] 45 | }).run(this.attributes); 46 | }, 47 | /** 48 | * @function 49 | * 50 | * Informs Bookshelf.js that the `games` property will be a list of 51 | * [models/game] models with a `tournamentId` that 52 | * matches the `id` specified in the query. 53 | **/ 54 | games: function(){ 55 | var Game = require("./game"); 56 | return this.hasMany(Game,"tournamentId"); 57 | } 58 | }); 59 | 60 | module.exports = Tournament; 61 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | var bookshelf = require("./bookshelf"); 2 | var checkit = require("checkit"); 3 | 4 | /** 5 | * @module {bookshelf.Model} models/user User 6 | * @parent bitballs.serviceModels 7 | * 8 | * @group models/user.properties 0 properties 9 | * 10 | * @signature `new User(properties)` 11 | * Creates an instance of a model. 12 | * 13 | * @param {Object} properties Initial values for this model's properties. 14 | */ 15 | 16 | var User = bookshelf.Model.extend( 17 | /** @prototype **/ 18 | { 19 | /** 20 | * @property {String<"users">} models/user.properties.tableName tableName 21 | * @parent models/user.properties 22 | * 23 | * Indicates which database table Bookshelf.js will query against. 24 | **/ 25 | tableName: 'users', 26 | /** 27 | * @function 28 | * 29 | * Binds to the "saving" event and specifies [models/user.prototype.validateSave validateSave] 30 | * as the handler during initialization. 31 | **/ 32 | initialize: function(){ 33 | this.on('saving', this.validateSave); 34 | }, 35 | /** 36 | * @function 37 | * 38 | * Validates that `email` is defined and formatted as an email address 39 | * and `password` is defiend on `this.attributes`. 40 | * 41 | * @return {Promise} 42 | **/ 43 | validateSave: function(){ 44 | return checkit({ 45 | email: ['required', 'email'], 46 | password: 'required' 47 | }).run(this.attributes); 48 | } 49 | }); 50 | 51 | module.exports = User; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitballs", 3 | "version": "0.4.1", 4 | "description": "A basketball tournament app", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "build": "node public/build.js", 9 | "develop": "node index.js --develop", 10 | "db-migrate": "db-migrate up", 11 | "document": "documentjs -d", 12 | "test": "npm run jshint && cd public/ && npm test", 13 | "jshint": "jshint ./ --config .jshintrc", 14 | "install": "node install.js && npm run db-migrate", 15 | "deploy": "firebase deploy", 16 | "deploy:ci": "firebase deploy --token \"$FIREBASE_TOKEN\"" 17 | }, 18 | "dependencies": { 19 | "async": "^2.4.1", 20 | "bcrypt-nodejs": "0.0.3", 21 | "body-parser": "^1.13.3", 22 | "bookshelf": "^0.10.3", 23 | "checkit": "^0.7.0", 24 | "cookie-parser": "^1.4.1", 25 | "db-migrate": "^0.9.26", 26 | "ejs": "^2.3.1", 27 | "express": "^4.15.3", 28 | "express-session": "^1.11.3", 29 | "knex": "^0.12.0", 30 | "lodash": "^4.17.4", 31 | "nodemailer": "^2.7.2", 32 | "passport": "^0.2.2", 33 | "passport-local": "^1.0.0", 34 | "pg": "4.5.6" 35 | }, 36 | "engines": { 37 | "node": "6.11.0" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/donejs/bitballs.git" 42 | }, 43 | "keywords": [ 44 | "node", 45 | "heroku", 46 | "express" 47 | ], 48 | "license": "MIT", 49 | "devDependencies": { 50 | "documentjs": "^0.4.4", 51 | "donejs": "^3.0.0", 52 | "donejs-cli": "^3.0.0", 53 | "firebase-tools": "^6.0.1", 54 | "jshint": "^2.9.1", 55 | "maildev": "^0.12.2" 56 | }, 57 | "urls": { 58 | "prod": "https://bitballs.herokuapp.com/", 59 | "dev": "http://localhost:5000/" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /public/app.less: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0 0 15px 0; 3 | font-family: "Helvetica Neue-Light","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif; 4 | font-weight: 300; 5 | background: url(./img/bitballs-logo-02.svg) no-repeat; 6 | background-size: 100% auto; 7 | } 8 | -------------------------------------------------------------------------------- /public/build.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var stealTools = require("steal-tools"); 3 | 4 | var config = { 5 | config: path.join(__dirname, "package.json!npm") 6 | }; 7 | 8 | module.exports = stealTools 9 | .build(config, { 10 | bundleAssets: true 11 | }); 12 | -------------------------------------------------------------------------------- /public/components/404.component: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /public/components/game/details/details.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /public/components/game/details/details.less: -------------------------------------------------------------------------------- 1 | game-details .stats-container { 2 | position: relative; 3 | width: 75%; 4 | border-left: 1px solid #ddd; 5 | border-right: 1px solid #ddd; 6 | } 7 | game-details .stat-point { 8 | font-family: Tahoma, Verdana, sans-serif; 9 | position: absolute; 10 | font-size: 8px; 11 | margin: 0 auto; 12 | text-align: center; 13 | display: inline-block; 14 | padding: 3px 2px; 15 | margin-bottom: 0; 16 | font-weight: normal; 17 | line-height: 1.5; 18 | text-align: center; 19 | white-space: nowrap; 20 | vertical-align: middle; 21 | -ms-touch-action: manipulation; 22 | touch-action: manipulation; 23 | cursor: pointer; 24 | -webkit-user-select: none; 25 | -moz-user-select: none; 26 | -ms-user-select: none; 27 | user-select: none; 28 | background-image: none; 29 | border: 1px solid transparent; 30 | border-radius: 2px; 31 | transform: translate( -50%, 0); 32 | 33 | .destroy-btn { 34 | display: none; 35 | padding: 2px; 36 | font-size: 0.9em; 37 | opacity: 0.5; 38 | 39 | &:hover { 40 | opacity: 1.0; 41 | } 42 | } 43 | } 44 | game-details .stat-point:hover { 45 | font-size: 12px; 46 | padding: 1px; 47 | 48 | .destroy-btn { 49 | display: inline-block; 50 | } 51 | } 52 | .youtube-container { 53 | padding-bottom: 15px; 54 | height: auto; 55 | #youtube-player { 56 | width: 100%; 57 | background: black; 58 | } 59 | } 60 | #player-pos { 61 | width: 1px; 62 | border-left: solid 1px #c46d3d; 63 | z-index: 0; 64 | position: absolute; 65 | } 66 | game-details { 67 | .stat-1P { 68 | background-color: #2d8e61; 69 | color: white; 70 | } 71 | .stat-1PA { 72 | background-color: #8cad9d; 73 | color: white; 74 | } 75 | .stat-2P { 76 | background-color: #2d8e61; 77 | color: white; 78 | } 79 | .stat-2PA { 80 | background-color: #8cad9d; 81 | color: white; 82 | } 83 | .stat-ORB { 84 | background-color: #9367a5; 85 | color: white; 86 | } 87 | .stat-DRB { 88 | color: #4E388C; 89 | border: solid 1px #9367a5; 90 | } 91 | .stat-Ast { 92 | background-color: #c46d3d; 93 | color: black; 94 | } 95 | 96 | .stat-Stl { 97 | border: solid 1px #1C2E8C; 98 | color: #1C2E8C; 99 | } 100 | .stat-Blk { 101 | border: solid 1px #4C2D0F; 102 | color: #4C2D0F; 103 | } 104 | .stat-To { 105 | background-color: red; 106 | color: white; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /public/components/game/details/details.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donejs/bitballs/9670ae729d4cbfe0900d20e654d2bf96aa402c6a/public/components/game/details/details.md -------------------------------------------------------------------------------- /public/components/game/details/details.stache: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{^ if(gamePromise.isRejected) }} 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |

{{ game.tournament.year }} - {{ game.round }} - Court {{ game.court }}

12 |

HOME: {{ game.homeTeam.color }} - {{ game.homeTeam.name }}

13 |

AWAY: {{ game.awayTeam.color }} - {{ game.awayTeam.name }}

14 |

Final Score {{ finalScore.home }} - {{ finalScore.away }}

15 |

Current Score {{ currentScore.home }} - {{ currentScore.away }}

16 |
17 | 18 |
19 | 20 | 21 | 22 | {{# each(game.teams) }} 23 | 24 | 25 | 26 | {{# each(players) }} 27 | 28 | 29 | 41 | 42 | {{/ each }} 43 | {{/ each }} 44 | 45 |
{{ color }} - {{ name }}
{{ name }} 30 | {{# each(scope.vm.statsForPlayerId(id)) }} 31 | 33 | {{ type }} 34 | {{# if(scope.vm.session.isAdmin()) }} 35 | 37 | {{/ if }} 38 | 39 | {{/ each }} 40 |
46 |
47 | 48 | 49 | {{# if(stat) }} 50 | 51 | 82 | 83 | {{/ if }} 84 | {{ else }} 85 | Game not found. 86 | {{/ if }} 87 | -------------------------------------------------------------------------------- /public/components/game/details/details_test.js: -------------------------------------------------------------------------------- 1 | import QUnit from "steal-qunit"; 2 | import Session from "~/models/session"; 3 | import { ViewModel as DetailsViewModel } from "./details"; 4 | import { games } from "~/models/fixtures/games"; 5 | import createGamesFixtures from "~/models/fixtures/games"; 6 | import F from 'funcunit'; 7 | import { fixture, stache, viewModel as canViewModel } from "can"; 8 | import $ from 'jquery'; 9 | import User from "~/models/user"; 10 | 11 | var deepEqual = QUnit.deepEqual, 12 | ok = QUnit.ok, 13 | notOk = QUnit.notOk; 14 | 15 | F.attach(QUnit); 16 | 17 | QUnit.module("bitballs/game/details/", { 18 | setup: function() { 19 | localStorage.clear(); 20 | fixture.delay = 1; 21 | createGamesFixtures(); 22 | this.vm = new DetailsViewModel({ 23 | gameId: 1, 24 | session: new Session() 25 | }); 26 | } 27 | }); 28 | 29 | QUnit.test("loads game data", function() { 30 | QUnit.stop(); 31 | 32 | this.vm.on("game", function(ev, game) { 33 | deepEqual(game.get(), games, "fetched game data matches fixture"); 34 | QUnit.start(); 35 | }); 36 | 37 | this.vm.gamePromise.catch(function(err) { 38 | ok(false, "game fetch failed"); 39 | QUnit.start(); 40 | }); 41 | }); 42 | 43 | QUnit.test("correctly sums score", function() { 44 | QUnit.stop(); 45 | 46 | var vm = this.vm; 47 | vm.on("game", function(ev, game) { 48 | deepEqual(vm.finalScore, { 49 | home: 3, 50 | away: 5 51 | }); 52 | QUnit.start(); 53 | }); 54 | }); 55 | 56 | QUnit.test("correctly sums the current score", function () { 57 | QUnit.stop(); 58 | var vm = this.vm; 59 | console.log("ON GAME"); 60 | vm.on('game', function whenGameIsLoaded () { 61 | console.log("GAME ON"); 62 | /* 63 | We assume each game starts with zero scores. 64 | So, no pickup games. 65 | */ 66 | vm.youtubePlayerTime = 0; 67 | console.log("TIME IS 0"); 68 | QUnit.deepEqual(vm.currentScore, { 69 | home: 0, 70 | away: 0 71 | }, 'Scores should zero at the beginning'); 72 | 73 | 74 | vm.youtubePlayerTime = Infinity; 75 | console.log("TIME IS INFINITY"); 76 | QUnit.deepEqual( 77 | vm.currentScore, 78 | vm.finalScore, 79 | 'At the end of the game, the current score is the final score' 80 | ); 81 | 82 | /* 83 | NOTE: this is a bad test because the home/away numbers are 84 | not described or easily inferred here. 85 | 86 | Given the current fixture data, we are summing like this: 87 | | Time | Home Points | Away Points | 88 | | 0 | 0 | 0 | 89 | | 20 | 1 | 0 | 90 | | 40 | 3 | 0 | 91 | | 60 | 3 | 1 | 92 | 93 | Therefore at time=50, home=3 and away=0. 94 | 95 | TODO: move the testing data out of remote fixtures. 96 | */ 97 | console.log("setting time to 50"); 98 | vm.youtubePlayerTime = 50; 99 | console.log("set time"); 100 | QUnit.deepEqual(vm.currentScore, { 101 | home: 3, 102 | away: 0 103 | }, 'Scores should reflect the sum for point stats'); 104 | 105 | QUnit.start(); 106 | }); 107 | }); 108 | 109 | 110 | QUnit.test('A stat can only be deleted by an admin', function () { 111 | var session = new Session({user: new User({ isAdmin: false }) }); 112 | 113 | var vm = this.vm; 114 | vm.session = session; 115 | var frag = stache('')(vm); 116 | 117 | $('#qunit-fixture').html(frag); 118 | 119 | F.confirm(true); 120 | 121 | F('.stat-point .destroy-btn') 122 | .size(0, 'There is no destroy button') 123 | .then(function () { 124 | vm.session.user.isAdmin = true; 125 | ok(true, 'The user is given admin privileges'); 126 | }) 127 | .size(6, 'Destroy buttons are inserted') 128 | .click() 129 | .size(5, 'Clicking the destroy button removed a stat'); 130 | }); 131 | 132 | 133 | QUnit.test('Deleting a stat does not change playback location', function (assert) { 134 | var done = assert.async(); 135 | var gotoCalled = false; 136 | var frag = stache('')({ 137 | gameId: this.vm.gameId, 138 | session: new Session({ 139 | user: new User({isAdmin: true}) 140 | }) 141 | }); 142 | 143 | $('#qunit-fixture').html(frag); 144 | 145 | 146 | var vm = canViewModel($('game-details')); 147 | var gotoTimeMinus5 = vm.__proto__.gotoTimeMinus5; // jshint ignore:line 148 | vm.__proto__.gotoTimeMinus5 = function (){ // jshint ignore:line 149 | gotoCalled = true; 150 | }; 151 | 152 | vm.on('game', function(ev, game) { 153 | F.confirm(true); 154 | F('.stat-point .destroy-btn') 155 | .exists('Destroy button exists') 156 | .click() 157 | .then(function () { 158 | notOk(gotoCalled, 'Seek was not called'); 159 | vm.__proto__.gotoTimeMinus5 = gotoTimeMinus5; // jshint ignore:line 160 | done(); 161 | }); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /public/components/game/details/test.html: -------------------------------------------------------------------------------- 1 | <game-details> tests 2 | 5 |
6 | -------------------------------------------------------------------------------- /public/components/navigation/img/bitballs-logo-01.svg: -------------------------------------------------------------------------------- 1 | bitballs-logo -------------------------------------------------------------------------------- /public/components/navigation/navigation.html: -------------------------------------------------------------------------------- 1 | <bitballs-navigation> 2 | 3 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /public/components/navigation/navigation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {Module} bitballs/components/navigation 3 | * @parent bitballs.components 4 | * 5 | * @group bitballs/components/navigation.properties 0 properties 6 | * 7 | * @description Provides navigation between different parts of the app 8 | * and lets a user login or logout. 9 | * 10 | * @signature `` 11 | * Creates the navigation for Bitballs. 12 | * 13 | * @param {bitballs/app} app The application viewModel. This component 14 | * will read and set the `session` property on the [bitballs/app]. 15 | * 16 | * 17 | * @body 18 | * 19 | * To create a `` element pass the [bitballs/models/session] 20 | * and a [bitballs/models/game] id like: 21 | * 22 | * ``` 23 | * 25 | * ``` 26 | * 27 | * ## Example 28 | * 29 | * @demo public/components/navigation/navigation.html 30 | * 31 | */ 32 | import { Component, DefineMap } from "can"; 33 | import Session from "bitballs/models/session"; 34 | import User from "bitballs/models/user"; 35 | import view from "./navigation.stache"; 36 | import $ from "jquery"; 37 | steal.loader.global.jQuery = $; 38 | 39 | import "bootstrap/dist/css/bootstrap.css"; 40 | import "bootstrap/js/dropdown"; 41 | import "./navigation.less"; 42 | 43 | 44 | var ViewModel = DefineMap.extend('NavigationVM', 45 | { 46 | /** 47 | * @property {bitballs/app} bitballs/components/navigation.app app 48 | * @parent bitballs/components/navigation.properties 49 | * 50 | * The [bitballs/app] used to add or destroy the session. 51 | */ 52 | app: 'any', 53 | /** 54 | * @property {Promise} bitballs/components/navigation.sessionPromise sessionPromise 55 | * @parent bitballs/components/navigation.properties 56 | * 57 | * The promise that resolves when the user is logged in. 58 | */ 59 | sessionPromise: 'any', 60 | /** 61 | * @property {bitballs/models/session} bitballs/models/session session 62 | * 63 | * Current session for the app 64 | */ 65 | session: Session, 66 | /** 67 | * @property {bitballs/models/session} bitballs/components/navigation.loginSession loginSession 68 | * @parent bitballs/components/navigation.properties 69 | * 70 | * A placeholder session with a nested [bitballs/models/user user] property that 71 | * is used for two-way binding the login form's username and password. 72 | */ 73 | loginSession: { 74 | default: function(){ 75 | return new Session({user: new User()}); 76 | } 77 | }, 78 | /** 79 | * @function createSession 80 | * 81 | * Creates the session on the server and when successful updates [bitballs/components/navigation.app] 82 | * with the session. Sets [bitballs/components/navigation.sessionPromise]. 83 | * @param {Event} [ev] Optional DOM event that will be prevented if passed. 84 | */ 85 | createSession: function(ev){ 86 | if(ev) { 87 | ev.preventDefault(); 88 | } 89 | var self = this; 90 | var sessionPromise = this.loginSession.save().then(function(session){ 91 | self.loginSession = new Session({user: new User()}); 92 | self.app.session = session; 93 | }); 94 | this.sessionPromise = sessionPromise; 95 | }, 96 | /** 97 | * @function logout 98 | * 99 | * Destroys [bitballs/components/navigation.app]'s [bitballs/models/session] and 100 | * then removes it from the session. 101 | */ 102 | logout: function(){ 103 | var sessionPromise = this.app.session.destroy(); 104 | this.sessionPromise = sessionPromise; 105 | this.app.session = null; 106 | }, 107 | /** 108 | * @function closeDropdown 109 | * Closes the dropdown. Needed for when someone clicks on register. 110 | */ 111 | closeDropdown: function ( el ) { 112 | $( el ).closest( ".session-menu" ).find( ".open .dropdown-toggle" ).dropdown( "toggle" ); 113 | } 114 | }); 115 | 116 | const Navigation = Component.extend({ 117 | tag: "bitballs-navigation", 118 | view, 119 | ViewModel 120 | }); 121 | 122 | export { Navigation, Navigation as Component, ViewModel }; 123 | -------------------------------------------------------------------------------- /public/components/navigation/navigation.less: -------------------------------------------------------------------------------- 1 | bitballs-navigation { 2 | .nav > li { 3 | float: left; 4 | } 5 | 6 | .navbar-nav { 7 | margin: 0; 8 | float: left; 9 | 10 | > li { 11 | 12 | > a { 13 | padding-top: 15px; 14 | padding-bottom: 15px; 15 | } 16 | 17 | > .dropdown-menu { 18 | position: absolute !important; 19 | top: 100%; 20 | right: 0; 21 | left: auto; 22 | background-color: #fff !important; 23 | border-radius: 4px !important; 24 | border: 1px solid rgba(0, 0, 0, .15) !important; 25 | box-shadow: 0 6px 12px rgba(0, 0, 0, .175) !important; 26 | 27 | form { 28 | padding: 10px; 29 | margin-bottom: 0px; 30 | } 31 | 32 | input { 33 | width: 150px 34 | } 35 | } 36 | } 37 | } 38 | 39 | .navbar-right { 40 | float: right; 41 | } 42 | 43 | .navbar { 44 | border-radius: none; 45 | } 46 | 47 | .navbar.navbar-default { 48 | background-color: #c46d3d; 49 | border: none; 50 | border-top-left-radius: 0; 51 | border-top-right-radius: 0; 52 | } 53 | 54 | .navbar-default .navbar-nav > li > a { 55 | color: white; 56 | } 57 | 58 | .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus, .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { 59 | background-color: rgba(45, 49, 53, .3); 60 | color: white; 61 | } 62 | 63 | .main-logo { 64 | padding: 15px; 65 | background: url(./img/bitballs-logo-01.svg) no-repeat 0 50%; 66 | background-size: 90% auto; 67 | width: 150px; 68 | text-indent: -9999px; 69 | overflow: hidden; 70 | } 71 | 72 | .dropdown-menu form { 73 | padding: 5px; 74 | margin-bottom: 0px; 75 | } 76 | } -------------------------------------------------------------------------------- /public/components/navigation/navigation.stache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 78 | -------------------------------------------------------------------------------- /public/components/navigation/navigation_test.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import stache from 'can-stache'; 3 | import QUnit from 'steal-qunit'; 4 | import F from 'funcunit'; 5 | import testUtils from 'bitballs/test/utils'; 6 | import './navigation'; 7 | 8 | F.attach(QUnit); 9 | 10 | QUnit.module('components/navigation/', { 11 | beforeEach: function () { 12 | var frag = stache('')(); 13 | testUtils.insertAndPopulateIframe('#qunit-fixture', frag); 14 | }, 15 | afterEach: function () { 16 | $('#qunit-fixture').empty(); 17 | } 18 | }); 19 | 20 | QUnit.test('Layout preserved at smaller screen resolutions', function (assert) { 21 | var evaluateAtWidth = function (resolution) { 22 | // Set the width 23 | F('#qunit-fixture iframe').then(function () { 24 | 25 | // For some reason the query needs to be redone 26 | $(this.selector).css('width', resolution); 27 | }); 28 | 29 | // Confirm the styles 30 | F('.session-menu') 31 | .visible('Session menu is visible at ' + resolution) 32 | .css('float', 'right', 33 | 'Session menu is floated right at ' + resolution); 34 | F('.main-menu') 35 | .visible('Main menu is visible at ' + resolution) 36 | .css('float', 'left', 37 | 'Main menu is floated left at ' + resolution); 38 | }; 39 | 40 | evaluateAtWidth('1170px'); 41 | evaluateAtWidth('750px'); 42 | }); 43 | 44 | QUnit.test('Register button exists', function () { 45 | var frag = stache('')(); 46 | var buttons = $(frag).find('.register-btn'); 47 | 48 | QUnit.equal(buttons.length, 1, 'Register button found'); 49 | }); 50 | -------------------------------------------------------------------------------- /public/components/navigation/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /public/components/player/details/details-test.js: -------------------------------------------------------------------------------- 1 | import QUnit from 'steal-qunit'; 2 | import F from 'funcunit'; 3 | import {ViewModel} from './details'; 4 | import { fixture } from 'can'; 5 | import stats from '../../../models/fixtures/stats'; 6 | 7 | F.attach(QUnit); 8 | 9 | // ViewModel unit tests 10 | QUnit.module('bitballs/components/player/details', { 11 | beforeEach: function(){ 12 | localStorage.clear(); 13 | this.stats = stats; 14 | } 15 | }); 16 | 17 | // Make sure we properly map stats to game ID's 18 | QUnit.test('should map stats to game ID', function (assert) { 19 | const GAME_ID = 40; 20 | const STATS = [{"id":3,"gameId":1,"playerId":3,"type":"1PA","time":43,"value":null},{"id":7,"gameId":1,"playerId":3,"type":"To","time":97,"value":null},{"id":27,"gameId":1,"playerId":3,"type":"2P","time":449,"value":null},{"id":37,"gameId":1,"playerId":3,"type":"1PA","time":723,"value":null},{"id":48,"gameId":1,"playerId":3,"type":"1PA","time":908,"value":null},{"id":53,"gameId":1,"playerId":3,"type":"1P","time":1066,"value":null},{"id":58,"gameId":1,"playerId":3,"type":"1PA","time":1175,"value":null},{"id":490,"gameId":15,"playerId":3,"type":"1PA","time":74,"value":null},{"id":526,"gameId":15,"playerId":3,"type":"DRB","time":378,"value":null},{"id":539,"gameId":15,"playerId":3,"type":"DRB","time":512,"value":null},{"id":540,"gameId":15,"playerId":3,"type":"1PA","time":519,"value":null},{"id":557,"gameId":15,"playerId":3,"type":"ORB","time":629,"value":null},{"id":561,"gameId":15,"playerId":3,"type":"DRB","time":640,"value":null},{"id":566,"gameId":15,"playerId":3,"type":"ORB","time":702,"value":null},{"id":567,"gameId":15,"playerId":3,"type":"Ast","time":706,"value":null},{"id":570,"gameId":15,"playerId":3,"type":"Ast","time":744,"value":null},{"id":583,"gameId":15,"playerId":3,"type":"ORB","time":810,"value":null},{"id":584,"gameId":15,"playerId":3,"type":"1PA","time":811,"value":null},{"id":936,"gameId":20,"playerId":3,"type":"DRB","time":318,"value":null},{"id":951,"gameId":20,"playerId":3,"type":"DRB","time":402,"value":null},{"id":953,"gameId":20,"playerId":3,"type":"1P","time":409,"value":null},{"id":1148,"gameId":23,"playerId":3,"type":"DRB","time":8,"value":null},{"id":1153,"gameId":23,"playerId":3,"type":"DRB","time":63,"value":null},{"id":1158,"gameId":23,"playerId":3,"type":"DRB","time":85,"value":null},{"id":1159,"gameId":23,"playerId":3,"type":"1P","time":88,"value":null},{"id":1192,"gameId":23,"playerId":3,"type":"1PA","time":343,"value":null},{"id":1195,"gameId":23,"playerId":3,"type":"DRB","time":351,"value":null},{"id":1202,"gameId":23,"playerId":3,"type":"1P","time":397,"value":null},{"id":1920,"gameId":35,"playerId":3,"type":"DRB","time":150,"value":null},{"id":1945,"gameId":35,"playerId":3,"type":"DRB","time":389,"value":null},{"id":1947,"gameId":35,"playerId":3,"type":"ORB","time":399,"value":null},{"id":1949,"gameId":35,"playerId":3,"type":"ORB","time":406,"value":null},{"id":1956,"gameId":35,"playerId":3,"type":"DRB","time":471,"value":null},{"id":1961,"gameId":35,"playerId":3,"type":"2PA","time":488,"value":null},{"id":1964,"gameId":35,"playerId":3,"type":"DRB","time":499,"value":null},{"id":1971,"gameId":35,"playerId":3,"type":"1P","time":571,"value":null},{"id":2283,"gameId":38,"playerId":3,"type":"1PA","time":61,"value":null},{"id":2291,"gameId":38,"playerId":3,"type":"1PA","time":108,"value":null},{"id":2304,"gameId":38,"playerId":3,"type":"DRB","time":246,"value":null},{"id":2308,"gameId":38,"playerId":3,"type":"DRB","time":266,"value":null},{"id":2310,"gameId":38,"playerId":3,"type":"ORB","time":277,"value":null},{"id":2311,"gameId":38,"playerId":3,"type":"1P","time":279,"value":null},{"id":2330,"gameId":38,"playerId":3,"type":"2PA","time":442,"value":null},{"id":2336,"gameId":38,"playerId":3,"type":"Blk","time":489,"value":null},{"id":2341,"gameId":38,"playerId":3,"type":"ORB","time":590,"value":null},{"id":2343,"gameId":38,"playerId":3,"type":"2P","time":596,"value":null},{"id":2346,"gameId":38,"playerId":3,"type":"1PA","time":625,"value":null},{"id":2350,"gameId":38,"playerId":3,"type":"ORB","time":652,"value":null},{"id":2373,"gameId":38,"playerId":3,"type":"DRB","time":842,"value":null},{"id":2377,"gameId":38,"playerId":3,"type":"2PA","time":894,"value":null},{"id":2385,"gameId":38,"playerId":3,"type":"To","time":954,"value":null},{"id":2639,"gameId":40,"playerId":3,"type":"1PA","time":42,"value":null}]; 21 | const WANTED_STATS = STATS.filter(({gameId}) => gameId === GAME_ID); 22 | 23 | fixture({ 24 | 'GET /services/games': { 25 | "data":[ 26 | { 27 | "id": GAME_ID, 28 | "tournamentId":1, 29 | "round":"Semi Finals", 30 | "court":"2", 31 | "videoUrl":"is2Z6JU6nGg", 32 | "homeTeamId":18, 33 | "awayTeamId":2 34 | }, 35 | ] 36 | }, 37 | 'GET /services/stats': { 38 | "data": STATS 39 | }, 40 | }); 41 | 42 | let done = assert.async(); 43 | let vm = new ViewModel({ 44 | playerId: '1' 45 | }); 46 | 47 | vm.on('statsByTournament', function(e, val){ 48 | assert.deepEqual(val[1].serialize(), WANTED_STATS); 49 | done(); 50 | }); 51 | }); -------------------------------------------------------------------------------- /public/components/player/details/details.html: -------------------------------------------------------------------------------- 1 | 5 | 7 | -------------------------------------------------------------------------------- /public/components/player/details/details.js: -------------------------------------------------------------------------------- 1 | import { Component, DefineMap } from 'can'; 2 | import './details.less'; 3 | import view from './details.stache'; 4 | import Game from 'bitballs/models/game'; 5 | import Player from 'bitballs/models/player'; 6 | import Stat from 'bitballs/models/stat'; 7 | import Tournament from 'bitballs/models/tournament'; 8 | 9 | export const ViewModel = DefineMap.extend({ 10 | /** 11 | * @property {Promise} bitballs/components/player/details.playerPromise playerPromise 12 | * @parent bitballs/components/player/details.properties 13 | * 14 | * A promise that fetches a [bitballs/models/player player] based on 15 | * [bitballs/components/player/details.ViewModel.prototype.playerId playerId]. 16 | **/ 17 | get playerPromise() { 18 | return Player.get(this.playerId); 19 | }, 20 | /** 21 | * @property {bitballs/models/player} bitballs/components/player/details.player player 22 | * @parent bitballs/components/player/details.properties 23 | * 24 | * A [bitballs/models/player player] instance. 25 | **/ 26 | player: { 27 | get: function(lastSet, setVal){ 28 | this.playerPromise.then(setVal); 29 | } 30 | }, 31 | /** 32 | * @property {Promise} bitballs/components/player/details.tournamentPromise tournamentsPromise 33 | * @parent bitballs/components/player/details.properties 34 | * 35 | * A promise that fetches a [bitballs/models/tournament.static.List tournament List] based on 36 | * [bitballs/components/player/details.ViewModel.prototype.playerId playerId]. 37 | **/ 38 | get tournamentsPromise() { 39 | return Tournament.getList(); 40 | }, 41 | /** 42 | * @property {bitballs/models/tournament.static.List} bitballs/components/player/details.tournament tournament 43 | * @parent bitballs/components/player/details.properties 44 | * 45 | * A [bitballs/models/tournament.static.List tournament List] instance. 46 | **/ 47 | tournaments: { 48 | get: function(lastSet, setVal){ 49 | this.tournamentsPromise.then(setVal); 50 | } 51 | }, 52 | /** 53 | * @property {Promise} bitballs/components/player/details.gamePromise gamesPromise 54 | * @parent bitballs/components/player/details.properties 55 | * 56 | * A promise that fetches a [bitballs/models/game.static.List game List] based on 57 | * [bitballs/components/player/details.ViewModel.prototype.playerId playerId]. 58 | **/ 59 | get gamesPromise() { 60 | return Game.getList(); 61 | }, 62 | /** 63 | * @property {bitballs/models/game.static.List} bitballs/components/player/details.game game 64 | * @parent bitballs/components/player/details.properties 65 | * 66 | * A [bitballs/models/game.static.List game List] instance. 67 | **/ 68 | games: { 69 | get: function(lastSet, setVal){ 70 | this.gamesPromise.then(setVal); 71 | } 72 | }, 73 | statTypes: { 74 | default: () => Stat.statTypes 75 | }, 76 | /** 77 | * @property {Promise} bitballs/components/player/details.statPromise statsPromise 78 | * @parent bitballs/components/player/details.properties 79 | * 80 | * A promise that fetches a [bitballs/models/stat.static.List stat List] based on 81 | * [bitballs/components/player/details.ViewModel.prototype.playerId playerId]. 82 | **/ 83 | get statsPromise() { 84 | return Stat.getList({ 85 | where: {playerId: this.playerId}, 86 | withRelated: [ 87 | 'game.tournament' 88 | ] 89 | }); 90 | }, 91 | /** 92 | * @property {bitballs/models/stat.static.List} bitballs/components/player/details.stat stat 93 | * @parent bitballs/components/player/details.properties 94 | * 95 | * A [bitballs/models/stat.static.List stat List] instance. 96 | **/ 97 | stats: { 98 | get: function(lastSet, setVal){ 99 | this.statsPromise.then(setVal); 100 | } 101 | }, 102 | 103 | get tournamentStats() { 104 | if (!this.stats) { 105 | return null; 106 | } 107 | 108 | let playerTournaments = []; 109 | this.stats.forEach((stat) => { 110 | let statTournament = stat.game.tournament; 111 | statTournament.year = new Date(statTournament.date).getFullYear(); 112 | 113 | let tournament = playerTournaments.find((tournament) => tournament.id === statTournament.id); 114 | let statModel = new Stat(stat); 115 | if(tournament) { 116 | tournament.stats.push(statModel); 117 | } 118 | else { 119 | statTournament.stats = new Stat.List([statModel]); 120 | playerTournaments.push(statTournament); 121 | } 122 | }); 123 | return playerTournaments; 124 | }, 125 | 126 | get statsByTournament() { 127 | if (!this.games || !this.stats || !this.tournaments) { 128 | return null; 129 | } 130 | 131 | let mapGamesToTournaments = {}; 132 | this.games.forEach(({ id, tournamentId }) => { 133 | mapGamesToTournaments[id] = tournamentId; 134 | }); 135 | 136 | let statsByTournament = []; 137 | 138 | this.stats.forEach((stat) => { 139 | let tournamentId = mapGamesToTournaments[stat.gameId]; 140 | if(tournamentId){ 141 | if (!statsByTournament[tournamentId]) { 142 | statsByTournament[tournamentId] = new Stat.List(); 143 | } 144 | 145 | statsByTournament[tournamentId].push(stat); 146 | } 147 | }); 148 | return statsByTournament; 149 | }, 150 | }); 151 | 152 | export const PlayerDetails = Component.extend({ 153 | tag: 'player-details', 154 | ViewModel, 155 | view 156 | }); 157 | 158 | export { PlayerDetails as Component }; 159 | -------------------------------------------------------------------------------- /public/components/player/details/details.less: -------------------------------------------------------------------------------- 1 | @border: 1px solid #ddd; 2 | 3 | player-details { 4 | display: block; 5 | h1 { 6 | margin-bottom: 20px; 7 | } 8 | h1, h2 { 9 | font-family: "Helvetica Neue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif 10 | } 11 | .player-stats { 12 | li { 13 | border-left: @border; 14 | width: 140px; 15 | padding: 0 20px; 16 | text-align: center; 17 | &:last-child { 18 | border-right: @border; 19 | } 20 | } 21 | } 22 | 23 | .tournament-stats { 24 | border-bottom: @border; 25 | padding-bottom: 10px; 26 | li { 27 | padding: 0 35px 10px 0; 28 | p { 29 | margin-bottom: 0; 30 | } 31 | } 32 | } 33 | 34 | .player-stats, .tournament-stats { 35 | display: flex; 36 | flex-direction: row; 37 | padding-left: 0; 38 | list-style: none; 39 | margin-bottom: 30px; 40 | li { 41 | p { 42 | font-size: 16px; 43 | font-weight: 400; 44 | color: #c46d3d; 45 | } 46 | div { 47 | font-size: 30px; 48 | font-weight: 500; 49 | } 50 | } 51 | } 52 | span.stats { 53 | padding: 10px 15px; 54 | } 55 | table.stats { 56 | border: solid 1px #000; 57 | th, td { 58 | padding: 10px 15px; 59 | border: solid 1px #000; 60 | } 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /public/components/player/details/details.md: -------------------------------------------------------------------------------- 1 | @parent bitballs 2 | @module {can.Component} bitballs/components/player/details 3 | 4 | A short description of the player-details component 5 | 6 | @signature `` 7 | 8 | @body 9 | 10 | ## Use 11 | 12 | -------------------------------------------------------------------------------- /public/components/player/details/details.stache: -------------------------------------------------------------------------------- 1 | {{# player }} 2 |

{{ name }}

3 |
    4 |
  • 5 |
    {{ age }}
    6 |

    Age

    7 |
  • 8 |
  • 9 |
    {{ weight }}lbs
    10 |

    Weight

    11 |
  • 12 |
  • 13 |
    {{ height }}"
    14 |

    Height

    15 |
  • 16 |
17 | {{/ player }} 18 | 19 |

Overall Stats

20 | {{# for(statType of stats.aggregated) }} 21 | 22 | {{statType.name}}: {{statType.default}} 23 | {{/ for }} 24 | 25 |

Stats by Year

26 | 27 | 28 | 29 | 30 | {{# for(statType of stats.aggregated) }} 31 | 32 | {{/ for }} 33 | 34 | {{# for(tournament of tournamentStats) }} 35 | 36 | 37 | {{# for(stat of tournament.stats.aggregated)}} 38 | 39 | {{/ for}} 40 | 41 | {{/ for}} 42 |
Year{{statType.name}}
{{ tournament.year }}{{stat.default}}
43 | -------------------------------------------------------------------------------- /public/components/player/details/test.html: -------------------------------------------------------------------------------- 1 | bitballs/components/player/details 2 | 3 |
4 | -------------------------------------------------------------------------------- /public/components/player/edit/edit.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /public/components/player/edit/edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {Module} bitballs/components/player/edit 3 | * @parent bitballs.components 4 | * 5 | * @group bitballs/components/player/edit.properties 0 properties 6 | * 7 | * @description Provides an interface for editing the values of a 8 | * [bitballs/models/player] model. 9 | * 10 | * @signature `` 11 | * Creates a form with inputs for each property in a [bitballs/models/player] model. 12 | * 13 | * @param {Boolean} is-admin Configures whether or not admin specific 14 | * features are enabled. 15 | * 16 | * 17 | * @body 18 | * 19 | * To create a `` element pass a boolean like [bitballs/app.prototype.isAdmin]: 20 | * 21 | * ``` 22 | * 24 | * ``` 25 | * 26 | * ## Example 27 | * 28 | * @demo public/components/player/edit/edit.html 29 | * 30 | **/ 31 | import { Component, DefineMap } from "can"; 32 | import Player from "bitballs/models/player"; 33 | import view from "./edit.stache"; 34 | import "bootstrap/dist/css/bootstrap.css"; 35 | 36 | 37 | export const ViewModel = DefineMap.extend("PlayerEditVM", 38 | { 39 | /** 40 | * @property {Boolean} bitballs/components/player/edit.isAdmin isAdmin 41 | * @parent bitballs/components/player/edit.properties 42 | * 43 | * Configures whether or not admin specific features are enabled. 44 | **/ 45 | isAdmin: { 46 | type: 'boolean', 47 | default: false 48 | }, 49 | /** 50 | * @property {bitballs/models/player} bitballs/components/player/edit.player player 51 | * @parent bitballs/components/player/edit.properties 52 | * 53 | * The model that will be bound to the form. 54 | **/ 55 | player: { 56 | Type: Player, 57 | Default: Player 58 | }, 59 | /** 60 | * @property {Boolean} bitballs/components/player/edit.isNewPlayer isNewPlayer 61 | * @parent bitballs/components/player/edit.properties 62 | * 63 | * Whether the player has not been created yet. 64 | */ 65 | isNewPlayer: { 66 | get: function isNew() { 67 | return this.player.isNew(); 68 | } 69 | }, 70 | /** 71 | * @property {Promise} bitballs/components/player/edit.savePromise savePromise 72 | * @parent bitballs/components/player/edit.properties 73 | * 74 | * A [bitballs/models/player] model. 75 | */ 76 | savePromise: 'any', 77 | /** 78 | * @function savePlayer 79 | * 80 | * Creates/updates the player on the server and when successful sets [bitballs/components/player/edit.player] 81 | * to a new [bitballs/models/player] model. Fires a "saved" event. 82 | * 83 | * @param {Event} [ev] A DOM Level 2 event that [`preventDefault`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) 84 | * will be called on. 85 | * 86 | * @return {Promise} 87 | */ 88 | savePlayer: function(ev){ 89 | if (ev) { 90 | ev.preventDefault(); 91 | } 92 | 93 | var self = this; 94 | var player = this.player; 95 | var promise; 96 | 97 | if(player.isNew()) { 98 | promise = player.save().then(function(){ 99 | self.player = new Player(); 100 | }); 101 | } else { 102 | promise = player.save(); 103 | } 104 | 105 | promise.then(function(){ 106 | player.backup(); 107 | self.dispatch("saved"); 108 | }); 109 | 110 | this.savePromise = promise; 111 | 112 | return promise; 113 | }, 114 | /** 115 | * @function cancel 116 | * 117 | * Restores the [bitballs/models/player] model to its state prior to editing. 118 | * Fires a "canceled" event. 119 | */ 120 | cancel: function() { 121 | this.player = this.player.restore(); 122 | this.dispatch("canceled"); 123 | } 124 | }); 125 | 126 | export const PlayerEdit = Component.extend({ 127 | tag: "player-edit", 128 | view, 129 | ViewModel 130 | }); 131 | 132 | export { PlayerEdit as Component }; 133 | -------------------------------------------------------------------------------- /public/components/player/edit/edit.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donejs/bitballs/9670ae729d4cbfe0900d20e654d2bf96aa402c6a/public/components/player/edit/edit.md -------------------------------------------------------------------------------- /public/components/player/edit/edit.stache: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{# if(isAdmin) }} 4 |
5 | {{# if(isNewPlayer) }} 6 |

Create Player

7 | {{ else }} 8 |

Update Player

9 | {{/ if }} 10 |
11 | 12 | 14 |
15 |
16 | 17 | 23 |
24 |
25 | 26 | 32 |
33 |
34 | 35 | 41 |
42 | 43 | {{# unless(isNewPlayer) }} 44 | Cancel 45 | {{/ unless }} 46 | {{# if(savePromise.isRejected) }} 47 | {{# each(savePromise.reason.responseJSON) }} 48 |

{{ . }}

49 | {{/ each }} 50 | {{/ if }} 51 |
52 | {{/ if }} 53 | -------------------------------------------------------------------------------- /public/components/player/edit/edit_test.js: -------------------------------------------------------------------------------- 1 | import QUnit from 'steal-qunit'; 2 | import { DefineMap, stache } from "can"; 3 | import { ViewModel } from 'bitballs/components/player/edit/edit'; 4 | import Player from 'bitballs/models/player'; 5 | import F from 'funcunit'; 6 | import $ from "jquery"; 7 | import './edit'; 8 | 9 | import defineFixtures from 'bitballs/models/fixtures/players'; 10 | 11 | F.attach(QUnit); 12 | 13 | // viewmodel unit tests 14 | QUnit.module('components/player/edit/', function(hooks){ 15 | 16 | hooks.beforeEach(function(){ 17 | localStorage.clear(); 18 | defineFixtures(); 19 | }); 20 | 21 | 22 | 23 | QUnit.test('Tests are running', function(assert){ 24 | assert.ok( true, "Passed!" ); 25 | }); 26 | 27 | QUnit.test('Can create new ViewModel', function(assert){ 28 | var vm = new ViewModel(); 29 | vm.player.name = "Justin"; 30 | assert.ok( !!vm , "Passed!" ); 31 | }); 32 | 33 | QUnit.test("Create player", function(assert){ 34 | assert.expect(1); 35 | var done = assert.async(), 36 | player = { 37 | "name": "Test Player", 38 | "weight": 200, 39 | "height": 71, 40 | "birthday": "1980-01-01" 41 | }, 42 | playerModel = new Player(player), 43 | vm = new ViewModel({ 44 | player:playerModel 45 | }); 46 | 47 | vm.on("saved", function(){ 48 | player.id = 1; 49 | assert.deepEqual(player, playerModel.get(), "New player saved"); 50 | vm.unbind("saved"); 51 | done(); 52 | }); 53 | vm.savePlayer(); 54 | 55 | }); 56 | 57 | QUnit.test("Create player fails without name", function(assert){ 58 | assert.expect(2); 59 | var done = assert.async(), 60 | player = { 61 | "weight": 200, 62 | "height": 71, 63 | "birthday": "1980-01-01" 64 | }, 65 | playerModel = new Player(player), 66 | vm = new ViewModel({ 67 | player: playerModel 68 | }); 69 | 70 | vm.savePlayer(); 71 | vm.savePromise.then(function(resp, type) { 72 | done(); 73 | }, function(resp) { 74 | assert.equal(resp.status, '400'); 75 | assert.equal(resp.statusText, 'error'); 76 | done(); 77 | }); 78 | }); 79 | 80 | QUnit.test("Update player", function(assert){ 81 | assert.expect(1); 82 | var done = assert.async(), 83 | player = { 84 | "name": "Test Player", 85 | "weight": 200, 86 | "height": 71, 87 | "birthday": "1980-01-01", 88 | "id": 1 89 | }, 90 | playerModel = new Player(player), 91 | vm = new ViewModel({ 92 | player:playerModel 93 | }); 94 | 95 | //update player info 96 | vm.player.name = "Test Player (modified)"; 97 | 98 | vm.on("saved", function(){ 99 | player.name = "Test Player (modified)"; 100 | assert.deepEqual(vm.player.name, player.name, "Player updated"); 101 | vm.unbind("saved"); 102 | done(); 103 | }); 104 | 105 | vm.savePlayer(); 106 | 107 | }); 108 | 109 | QUnit.test('Properties are restored when canceled', function (assert) { 110 | var initialName = 'Chris Gomez'; 111 | var initialWeight = 175; 112 | var initialHeight = 69; 113 | var editedName = 'Alfred Hitchcock'; 114 | var editedWeight = 210; 115 | var editedHeight = 67; 116 | 117 | var vm = new ViewModel({ 118 | player: { 119 | name: initialName, 120 | weight: initialWeight, 121 | height: initialHeight 122 | } 123 | }); 124 | 125 | var player = vm.player; 126 | 127 | player.backup(); 128 | 129 | assert.equal(player.name, initialName, 'Initial name is correct'); 130 | assert.equal(player.weight, initialWeight, 'Initial weight is correct'); 131 | assert.equal(player.height, initialHeight, 'Initial height is correct'); 132 | 133 | player.name = editedName; 134 | player.weight = editedWeight; 135 | player.height = editedHeight; 136 | 137 | assert.equal(player.name, editedName, 'Edited name is correct'); 138 | assert.equal(player.weight, editedWeight, 'Edited weight is correct'); 139 | assert.equal(player.height, editedHeight, 'Edited height is correct'); 140 | 141 | vm.cancel(); 142 | 143 | assert.equal(player.name, initialName, 'Restored name is correct'); 144 | assert.equal(player.weight, initialWeight, 'Restored weight is correct'); 145 | assert.equal(player.height, initialHeight, 'Restored height is correct'); 146 | }); 147 | 148 | 149 | QUnit.test('Form is only shown to admins', function () { 150 | 151 | var vm = new DefineMap({ 152 | isAdmin: false 153 | }); 154 | var frag = stache('')(vm); 155 | 156 | QUnit.equal($('player-edit .edit-form', frag).length, 0, 157 | 'Form is excluded for non-admin user'); 158 | 159 | vm.isAdmin = true; 160 | 161 | QUnit.equal($('player-edit .edit-form', frag).length, 1, 162 | 'Form is included for admin user'); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /public/components/player/edit/test.html: -------------------------------------------------------------------------------- 1 | player/edit 2 | 3 |
4 | -------------------------------------------------------------------------------- /public/components/player/list/list.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 13 | -------------------------------------------------------------------------------- /public/components/player/list/list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {Module} bitballs/components/player/list 3 | * @parent bitballs.components 4 | * 5 | * @group bitballs/components/player/list.properties 0 properties 6 | * 7 | * @description Provides links to the existing [bitballs/models/player]s. Enables logged 8 | * in admin users to create, update, and destroy [bitballs/models/player]s. 9 | * 10 | * @signature `` 11 | * Renders a list of [bitballs/models/player] models. 12 | * 13 | * @param {Boolean} is-admin Configures whether or not admin specific 14 | * features are enabled. 15 | * 16 | * 17 | * @body 18 | * 19 | * To create a `` element pass a boolean like [bitballs/app.prototype.isAdmin]: 20 | * 21 | * ``` 22 | * 24 | * ``` 25 | * 26 | * ## Example 27 | * 28 | * @demo public/components/player/list/list.html 29 | * 30 | **/ 31 | import { Component, DefineMap } from "can"; 32 | import Player from "bitballs/models/player"; 33 | import view from "./list.stache"; 34 | import "bootstrap/dist/css/bootstrap.css"; 35 | 36 | export const ViewModel = DefineMap.extend('PlayerListVM', 37 | { 38 | /** 39 | * @property {Boolean} bitballs/components/player/list.isAdmin isAdmin 40 | * @parent bitballs/components/player/list.properties 41 | * 42 | * Configures whether or not admin specific features are enabled. 43 | **/ 44 | isAdmin: { 45 | type: 'boolean', 46 | default: false 47 | }, 48 | /** 49 | * @property {bitballs/models/Player} bitballs/models/player editingPlayer 50 | * 51 | * holds the current player instance that is being edited 52 | */ 53 | editingPlayer: {Type: Player, default: null}, 54 | /** 55 | * @property {Promise} bitballs/components/player/list.playersPromise playersPromise 56 | * @parent bitballs/components/player/list.properties 57 | * 58 | * A [bitballs/models/player] model List. 59 | */ 60 | playersPromise: { 61 | default: function(){ 62 | return Player.getList({orderBy: "name"}); 63 | } 64 | }, 65 | /** 66 | * @function editPlayer 67 | * 68 | * Selects a [bitballs/models/player] model for editing. 69 | * 70 | * @param {bitballs/models/player} player 71 | * The player model that will be passed to the `` 72 | * component. 73 | */ 74 | editPlayer: function(player){ 75 | player.backup(); 76 | this.editingPlayer = player; 77 | }, 78 | /** 79 | * @function removeEdit 80 | * 81 | * Deselects the [bitballs/models/player] model being edited. 82 | */ 83 | removeEdit: function(){ 84 | this.editingPlayer = null; 85 | }, 86 | /** 87 | * @function 88 | * @description Delete a player from the database. 89 | * @param {bitballs/models/player} player The [bitballs/models/player] to delete. 90 | * 91 | * @body 92 | * 93 | * Use in a template like: 94 | * ``` 95 | * 96 | * ``` 97 | */ 98 | deletePlayer: function (player) { 99 | if (! window.confirm('Are you sure you want to delete this player?')) { 100 | return; 101 | } 102 | player.destroy(); 103 | } 104 | }); 105 | 106 | export const PlayerList = Component.extend({ 107 | tag: "player-list", 108 | view, 109 | ViewModel 110 | }); 111 | 112 | export { PlayerList as Component }; 113 | -------------------------------------------------------------------------------- /public/components/player/list/list.stache: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Players

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {{# playersPromise.isPending }} 16 | 17 | {{/ playersPromise.isPending }} 18 | {{# if(playersPromise.isResolved) }} 19 | {{# each(playersPromise.value) }} 20 | 21 | {{# eq(this,../editingPlayer) }} 22 | 30 | {{ else }} 31 | 32 | 33 | 34 | 35 | 47 | {{/ eq }} 48 | 49 | {{ else }} 50 | 51 | {{/ each }} 52 | {{/ if }} 53 | 54 |
NameAgeWeightHeight
Loading
23 | 29 | {{ name }}{{ age }}{{ weight }}{{ height }} 36 | {{# if(../isAdmin) }} 37 | 40 | 45 | {{/ if }} 46 |
No Players
55 | {{# if(isAdmin) }} 56 | 57 | {{/ if }} 58 | -------------------------------------------------------------------------------- /public/components/player/list/list_test.js: -------------------------------------------------------------------------------- 1 | import QUnit from "steal-qunit"; 2 | import Player from "bitballs/models/player"; 3 | import { ViewModel } from "./list"; 4 | import defineFixtures from "bitballs/models/fixtures/players"; 5 | import F from "funcunit"; 6 | import { fixture, stache } from "can"; 7 | import $ from "jquery"; 8 | 9 | F.attach(QUnit); 10 | 11 | var vm; 12 | QUnit.module("components/player/list/", { 13 | beforeEach: function () { 14 | localStorage.clear(); 15 | fixture.delay = 1; 16 | defineFixtures(); 17 | vm = new ViewModel(); 18 | }, 19 | afterEach: function () { 20 | defineFixtures(); 21 | vm = undefined; 22 | } 23 | }); 24 | 25 | QUnit.test("players property loads players from server during instantiation", function (assert) { 26 | var done = assert.async(); 27 | vm.playersPromise.then(function (players) { 28 | assert.ok(players.length, "we got some players"); 29 | done(); 30 | }); 31 | }); 32 | 33 | QUnit.test("editPlayer sets editingPlayer to passed in player", function (assert) { 34 | var player = new Player({ name: "Ryan" }); 35 | vm.editPlayer(player); 36 | assert.deepEqual(vm.editingPlayer, player, "editingPlayer was set"); 37 | }); 38 | 39 | QUnit.test("removeEdit removes editingPlayer", function (assert) { 40 | var player = { name: "Ryan" }; 41 | vm.editingPlayer = player; 42 | vm.removeEdit(); 43 | assert.notOk(vm.editingPlayer, "editingPlayer was removed"); 44 | }); 45 | 46 | QUnit.test('Loading message shown while players list is loaded', function (assert) { 47 | var frag = stache('')(); 48 | 49 | var resolveFixture; 50 | 51 | $('#qunit-fixture').html(frag); 52 | 53 | fixture('GET /services/players', function (req, res) { 54 | resolveFixture = res; 55 | }); 56 | 57 | F('tbody tr.info') 58 | .exists('Loading element is present') 59 | .text('Loading', 'Loading message is shown') 60 | .then(function () { 61 | assert.ok(true, 'Request is resolved'); 62 | resolveFixture({ data: [] }); 63 | }) 64 | .closest('tbody') 65 | .size(0, 'Loading element was removed'); 66 | }); 67 | 68 | QUnit.test('Placeholder message is shown when player list is empty', function () { 69 | var frag = stache('')(); 70 | 71 | // Make the players fixture return an empty list 72 | fixture('GET /services/players', function () { 73 | return { data: [] }; 74 | }); 75 | 76 | $('#qunit-fixture').html(frag); 77 | 78 | F('tbody tr.empty-list-placeholder') 79 | .exists('Placeholder element is present'); 80 | }); 81 | -------------------------------------------------------------------------------- /public/components/player/list/test.html: -------------------------------------------------------------------------------- 1 | <player-list> tests 2 | 3 |
4 | -------------------------------------------------------------------------------- /public/components/test.js: -------------------------------------------------------------------------------- 1 | import './game/details/details_test'; 2 | // import './navigation/navigation_test'; // Commented out because this needs to attach funcunit to something else 3 | import './player/edit/edit_test'; 4 | import './player/list/list_test'; 5 | import './tournament/details/details_test'; 6 | import './tournament/list/list_test'; 7 | import './user/test'; 8 | import './player/details/details-test'; 9 | -------------------------------------------------------------------------------- /public/components/tournament/details/details.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 18 | -------------------------------------------------------------------------------- /public/components/tournament/details/details_test.js: -------------------------------------------------------------------------------- 1 | import QUnit from 'steal-qunit'; 2 | import { ViewModel } from './details'; 3 | import defineTournamentFixtures from 'bitballs/models/fixtures/tournaments'; 4 | import 'bitballs/models/fixtures/players'; 5 | import defineGameFixtures from 'bitballs/models/fixtures/games'; 6 | import clone from 'steal-clone'; 7 | import { DefineMap, fixture } from "can"; 8 | import Game from 'bitballs/models/game'; 9 | 10 | var vm; 11 | 12 | QUnit.module('components/tournament/details/', { 13 | beforeEach: function (assert) { 14 | let done = assert.async(); 15 | localStorage.clear(); 16 | defineTournamentFixtures(); 17 | defineGameFixtures(); 18 | 19 | 20 | clone({ 21 | 'bitballs/models/tournament': { 22 | 'default': { 23 | get: function() { 24 | console.log('we are here...'); 25 | return Promise.resolve(new DefineMap({ 26 | name: 'Test Name' 27 | })); 28 | } 29 | }, 30 | __useDefault: true 31 | } 32 | }) 33 | .import('./details') 34 | .then(({ ViewModel }) => { 35 | vm = new ViewModel({ 36 | tournamentId: 2 37 | }); 38 | done(); 39 | }); 40 | } 41 | }); 42 | 43 | QUnit.test('should load a tournament', (assert) => { 44 | let done = assert.async(); 45 | vm.on('tournament', function (ev, newVal) { 46 | assert.equal(newVal.name, 'Test Name', 'with the correct name' ); 47 | done(); 48 | }); 49 | }); 50 | 51 | QUnit.test('The selected round defaults to the first available round', function () { 52 | var vm = new ViewModel(); 53 | 54 | var gamesResponse = { data: [] }; 55 | 56 | Game.courtNames.forEach(function (courtName) { 57 | gamesResponse.data.push({ 58 | round: Game.roundNames[0], 59 | court: courtName 60 | }); 61 | }); 62 | 63 | fixture('/services/games', function() { 64 | return gamesResponse; 65 | }); 66 | 67 | vm.on("selectedRound", function(){}); 68 | QUnit.stop(); 69 | vm.gamesPromise.then(function (games) { 70 | QUnit.start(); 71 | QUnit.equal(vm.selectedRound, Game.roundNames[1], 72 | 'The second round is selected'); 73 | }); 74 | }); 75 | 76 | QUnit.test('The selected court defaults to the first available court', function () { 77 | var vm = new ViewModel(); 78 | 79 | var gamesResponse = { data: [{ 80 | round: Game.roundNames[0], 81 | court: Game.courtNames[0] 82 | }] }; 83 | 84 | fixture('/services/games', function() { 85 | return gamesResponse; 86 | }); 87 | 88 | QUnit.stop(); 89 | vm.on("selectedCourt", function(){}); 90 | vm.gamesPromise.then(function (games) { 91 | QUnit.start(); 92 | vm.on('selectedCourt', function(){}); 93 | QUnit.equal(vm.selectedCourt, Game.courtNames[1], 94 | 'The second court is selected'); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /public/components/tournament/details/test.html: -------------------------------------------------------------------------------- 1 | Tournament Details Tests 2 | 3 |
2 | 3 | 14 | -------------------------------------------------------------------------------- /public/components/tournament/list/list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {Module} bitballs/components/tournament/list 3 | * @parent bitballs.components 4 | * 5 | * @group bitballs/components/tournament/list.properties 0 properties 6 | * 7 | * @description Provides links to the existing tournaments. Enables logged 8 | * in admin users to create and destroy tournaments. 9 | * 10 | * @signature `` 11 | * Renders a list of tournaments. 12 | * 13 | * @param {Boolean} is-admin Configures whether or not admin specific 14 | * features are enabled. 15 | * 16 | * 17 | * @body 18 | * 19 | * To create a `` element pass a boolean like [bitballs/app.prototype.isAdmin]: 20 | * 21 | * ``` 22 | * 24 | * ``` 25 | * 26 | * ## Example 27 | * 28 | * @demo public/components/tournament/list/list.html 29 | * 30 | */ 31 | import { 32 | Component, DefineMap 33 | } from "can"; 34 | import Tournament from "bitballs/models/tournament"; 35 | import view from "./list.stache"; 36 | import "bootstrap/dist/css/bootstrap.css!"; 37 | import "can-stache-route-helpers"; 38 | 39 | export const ViewModel = DefineMap.extend('TournamentList', 40 | /** @prototype */ 41 | { 42 | 43 | tournamentsPromise: { 44 | default: function(){ 45 | return Tournament.getList({orderBy: "date"}); 46 | } 47 | }, 48 | /** 49 | * @property {bitballs/models/tournament} bitballs/components/tournament/list.tournament tournament 50 | * @parent bitballs/components/tournament/list.properties 51 | * 52 | * The [bitballs/models/tournament] model that backs the tournament 53 | * creation form. 54 | **/ 55 | tournament: { 56 | Type: Tournament, 57 | Default: Tournament 58 | }, 59 | /** 60 | * @property {Boolean} bitballs/components/tournament/list.isAdmin isAdmin 61 | * @parent bitballs/components/tournament/list.properties 62 | * 63 | * Configures whether or not admin specific features are enabled. 64 | **/ 65 | isAdmin: { 66 | type: 'boolean', 67 | default: false 68 | }, 69 | /** 70 | * @property {Promise} bitballs/components/tournament/list.savePromise savePromise 71 | * @parent bitballs/components/tournament/list.properties 72 | * 73 | * A promise that resolves when [bitballs/component/tournament/list.prototype.createTournament] 74 | * is called and the [bitballs/models/tournament] model is persisted to the server. 75 | **/ 76 | savePromise: 'any', 77 | /** 78 | * @function createTournament 79 | * 80 | * @description Creates the tournament on the server and when successful sets 81 | * [bitballs/components/tournament/list.tournament] to a new [bitballs/models/tournament] model. 82 | * 83 | * @param {Event} [ev] A DOM Level 2 event. 84 | * 85 | * @return {Promise} A [bitballs/models/tournament] model. 86 | */ 87 | createTournament: function(ev) { 88 | if (ev) { 89 | ev.preventDefault(); 90 | } 91 | var self = this; 92 | 93 | var promise = this.tournament.save().then(function(player) { 94 | self.tournament = new Tournament(); 95 | }); 96 | 97 | this.savePromise = promise; 98 | return promise; 99 | }, 100 | /** 101 | * @function 102 | * @description Delete a tournament from the database. 103 | * @param {bitballs/models/tournament} tournament The [bitballs/models/tournament] to delete. 104 | * 105 | * @body 106 | * 107 | * Use in a template like: 108 | * ``` 109 | * 110 | * ``` 111 | */ 112 | deleteTournament: function (tournament) { 113 | if (! window.confirm('Are you sure you want to delete this tournament?')) { 114 | return; 115 | } 116 | tournament.destroy(); 117 | } 118 | }); 119 | 120 | export const TournamentList = Component.extend({ 121 | tag: "tournament-list", 122 | view, 123 | ViewModel 124 | }); 125 | 126 | export { TournamentList as Component }; 127 | -------------------------------------------------------------------------------- /public/components/tournament/list/list.stache: -------------------------------------------------------------------------------- 1 |

Tournaments

2 | 3 | {{# if(tournamentsPromise.isPending) }} 4 |
Loading
5 | {{/ if }} 6 | {{# if(tournamentsPromise.isResolved) }} 7 | {{# each(tournamentsPromise.value) }} 8 |

{{ year }} 9 | {{# if(../isAdmin) }} 10 | 15 | {{/ if }} 16 |

17 | {{ else }} 18 |
No Tournaments
19 | {{/ each }} 20 | {{/ if }} 21 | 22 | {{# if(isAdmin) }} 23 |

New Tournament

24 |
25 |
26 | 27 | 33 |
34 | 35 | {{# if(savePromise.isRejected) }} 36 | {{# each(savePromise.reason.responseJSON) }} 37 |

{{ . }}

38 | {{/ each }} 39 | {{/ if }} 40 |
41 | {{/ if }} 42 | -------------------------------------------------------------------------------- /public/components/tournament/list/list_test.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import { stache, fixture } from "can"; 3 | import QUnit from 'steal-qunit'; 4 | import F from 'funcunit'; 5 | import { ViewModel } from './list'; 6 | import defineFixtures from 'bitballs/models/fixtures/tournaments'; 7 | 8 | F.attach(QUnit); 9 | 10 | QUnit.module('components/tournament/list/', { 11 | beforeEach: function () { 12 | localStorage.clear(); 13 | fixture.delay = 1; 14 | defineFixtures(); 15 | } 16 | }); 17 | 18 | QUnit.test('creating tournament fails without a name', function(assert){ 19 | var done = assert.async(); 20 | 21 | assert.expect(2); 22 | 23 | var vm = new ViewModel(); 24 | 25 | vm.createTournament(); 26 | vm.savePromise.then(done, function(resp, type){ 27 | assert.equal(resp.statusText, 'error', 'fail creation without date'); 28 | assert.equal(resp.status, '400', 'rejected'); 29 | done(); 30 | }); 31 | }); 32 | 33 | QUnit.test('Create button is disabled while posting data', function (assert) { 34 | var done = assert.async(); 35 | var expectingRequest = true; 36 | var vm = new ViewModel({ 37 | app: { 38 | isAdmin: true 39 | }, 40 | tournament: { 41 | name: 'Ballderdash', 42 | date: '01/21/1987' 43 | } 44 | }); 45 | 46 | 47 | var frag = stache('')(vm); 48 | var resolveRequest; 49 | 50 | fixture('POST /services/tournaments', function (req, res) { 51 | QUnit.ok(expectingRequest, 'Request was made'); 52 | 53 | // Determine when the request resolves, later 54 | resolveRequest = res; 55 | 56 | // The request should only be made once 57 | expectingRequest = false; 58 | }); 59 | 60 | $('#qunit-fixture').html(frag); 61 | 62 | // Click the button multiple times and ensure it's disabled 63 | // during requests 64 | F('tournament-list .create-btn') 65 | .visible('Create button is visible') 66 | .attr('disabled', undefined, 'Create button is enabled') 67 | .click(); 68 | F('tournament-list .create-btn') 69 | .attr('disabled', 'disabled', 'Create button is disabled') 70 | .then(function() { 71 | resolveRequest({id: 9910911}); 72 | }) 73 | .attr('disabled', undefined, 74 | 'Create button is enabled after the request is resolved').then(function(){ 75 | done(); 76 | }); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /public/components/tournament/list/test.html: -------------------------------------------------------------------------------- 1 | Tournament Tests 2 | 4 |
2 | 3 | 4 | 5 | 6 | 7 | {{# if(session.user) }} 8 | {{# unless(session.user.verified) }} 9 |
10 |

User Verification

11 | 16 | {{/ unless }} 17 | {{/ if }} 18 | 19 | 20 | 21 | 65 | -------------------------------------------------------------------------------- /public/components/user/details/details.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {Module} bitballs/components/user/details 3 | * @parent bitballs.components 4 | * 5 | * @description Provides a custom element that allows a user to 6 | * register, to view account verification status, and to 7 | * update their password. 8 | * 9 | * @signature `` 10 | * Creates the user details form. 11 | * 12 | * @param {bitballs/model/session} session The session object. If a user is 13 | * currently logged in, contains data about that user. 14 | * 15 | * 16 | * @body 17 | * 18 | * To create a `` element, pass the [bitballs/model/session] like: 19 | * 20 | * ``` 21 | * 24 | * ``` 25 | * 26 | * ## Example 27 | * 28 | * @demo public/components/user/details/details.html 29 | */ 30 | 31 | import { Component, DefineMap, route } from "can"; 32 | import User from "bitballs/models/user"; 33 | import Session from "bitballs/models/session"; 34 | import "bootstrap/dist/css/bootstrap.css"; 35 | import view from "./details.stache!"; 36 | 37 | /** 38 | * @constructor bitballs/components/user/details.ViewModel ViewModel 39 | * @parent bitballs/components/user/details 40 | * 41 | * @description A `` component's viewModel. 42 | */ 43 | 44 | export const ViewModel = DefineMap.extend({ 45 | /** 46 | * @property {bitballs/models/session|null} 47 | * 48 | * If a user is logged in, the session data, including 49 | * data about the currently logged in user. 50 | * 51 | * @signature `bitballs/models/session` 52 | * 53 | * A session instance, which includes data about the logged in user like: 54 | * 55 | * { 56 | * user: { 57 | * email: "tomrobbins@tommyrotten.net", 58 | * id: 4, 59 | * verified: false, 60 | * isAdmin: false 61 | * } 62 | * } 63 | * 64 | * @signature `null` 65 | * 66 | * If the user is not currently logged in, `null`. 67 | */ 68 | session: { 69 | default: null 70 | }, 71 | /** 72 | * @property {Promise} bitballs/components/user/details.savePromise savePromise 73 | * @parent bitballs/components/users/details.properties 74 | * 75 | * The promise that resolves when the user is saved 76 | */ 77 | savePromise: 'any', 78 | /** 79 | * @property {can-define} 80 | * 81 | * Provides a user instance. If a session is active, this 82 | * syncs the user with `session.user`. Otherwise, a user instance 83 | * is created since this property is used to bind with the user details form. 84 | * 85 | */ 86 | user: { 87 | Default: User, 88 | get: function(val) { 89 | if (this.session) { 90 | return this.session.user; 91 | } 92 | return val; 93 | } 94 | }, 95 | /** 96 | * @property {String} 97 | * 98 | * The status of the user. One of the following: 99 | * 100 | * - "new": user has not been created 101 | * - "pending": user has been created, but has not verified their email address 102 | * - "verified": user has verified their email address 103 | * 104 | * With a new user, the component shows a registration form. 105 | * With a pending user, the component shows the email address. 106 | * With a verified user, the component shows a form allowing the user to change their password. 107 | */ 108 | get userStatus() { 109 | if (this.user.isNew()) { 110 | return "new"; 111 | } 112 | if (!this.user.verified) { 113 | return "pending"; 114 | } 115 | return "verified"; 116 | }, 117 | /** 118 | * @property {Boolean} 119 | * 120 | * Whether the user has not been created yet. 121 | */ 122 | isNewUser: { 123 | get: function get() { 124 | return this.user.isNew(); 125 | } 126 | }, 127 | /** 128 | * @function saveUser 129 | * 130 | * If the user is being created, creates a new user and when successful: 131 | * - Creates a new session 132 | * - Logs the new user in 133 | * - Changes the page route from "register" to "account" 134 | * 135 | * If the user's password is being updated, updates the password and 136 | * when successful, clears the form. 137 | * 138 | * @return {Promise<>} A promise that allows the component to display errors, if any. 139 | * 140 | */ 141 | saveUser: function(ev) { 142 | if(ev) { ev.preventDefault(); } 143 | var self = this, 144 | isNew = this.user.isNew(), 145 | promise = this.user.save().then(function(user) { 146 | user.password = ""; 147 | user.verificationHash = ""; 148 | user.newPassword = null; 149 | 150 | if (!self.session) { 151 | self.session = new Session({ 152 | user: user 153 | }); 154 | } else { 155 | self.session.user = user; 156 | } 157 | if (isNew) { 158 | route.page = "account"; 159 | } 160 | }); 161 | this.savePromise = promise; 162 | return promise; 163 | }, 164 | /** 165 | * @function deleteUser 166 | * 167 | * Confirms that the user would like to delete his or her account, then 168 | * destroys the user and when successful: 169 | * - Logs the user out, destroying the current session 170 | * - Changes the page route from "account" to "register" 171 | */ 172 | deleteUser: function() { 173 | var self = this; 174 | if (confirm('Are you sure you want to delete your account?')) { 175 | this.user.destroy(function() { 176 | self.session.destroy(); 177 | self.session = null; 178 | route.page = "register"; 179 | }); 180 | } 181 | } 182 | }); 183 | 184 | export const UserDetails = Component.extend({ 185 | tag: "user-details", 186 | view, 187 | ViewModel 188 | }); 189 | 190 | export { UserDetails as Component }; 191 | -------------------------------------------------------------------------------- /public/components/user/details/details.stache: -------------------------------------------------------------------------------- 1 |

2 | {{# switch(userStatus) }} 3 | {{# case("new") }} 4 | Register New User 5 | {{/ case }} 6 | 7 | {{# case("pending") }} 8 | Verify User 9 | {{/ case }} 10 | 11 | {{# default }} 12 | Update User 13 | {{/ default }} 14 | {{/ switch }} 15 |

16 |
17 |
18 | 21 | {{# is(userStatus, "verified") }} 22 |
23 | verified! 24 | 29 |
30 | {{ else }} 31 | 36 | {{/ is }} 37 |
38 | 39 | {{# is(userStatus, "pending") }} 40 |
41 | 42 |
43 | {{ else }} 44 |
45 | 52 | 57 |
58 | {{# unless(isNewUser) }} 59 |
60 | 63 | 68 |
69 | {{/ unless }} 70 | 73 | {{/ is }} 74 | 75 | {{# unless(isNewUser) }} 76 |
77 |
78 |
DELETE ACCOUNT
79 | {{/ unless }} 80 | 81 | {{# if(savePromise.isRejected) }} 82 | {{# each(savePromise.reason.responseJSON) }} 83 |

{{ . }}

84 | {{/ each }} 85 | {{/ if }} 86 |
87 | -------------------------------------------------------------------------------- /public/components/user/details/details_test.js: -------------------------------------------------------------------------------- 1 | import QUnit from 'steal-qunit'; 2 | import { ViewModel } from 'bitballs/components/user/details/'; 3 | import 'bitballs/models/fixtures/users'; 4 | 5 | QUnit.module('components/user/', { 6 | beforeEach: function() { 7 | } 8 | }); 9 | 10 | QUnit.test('create new user', function(assert) { 11 | assert.expect(5); 12 | var done = assert.async(); 13 | 14 | var vm = new ViewModel(); 15 | 16 | vm.user.set({ 17 | email: 'test@bitovi.com', 18 | password: '123' 19 | }); 20 | 21 | // session is not created before user is saved: 22 | assert.ok(vm.user.isNew(), 'User should be new.'); 23 | 24 | assert.equal(vm.session, null, 'Session should not exist before user gets created.'); 25 | 26 | vm.saveUser().then(function(){ 27 | assert.equal(vm.session.user.email, 'test@bitovi.com', 'Session email should be set after user gets created.'); 28 | assert.notOk(vm.user.isNew(), 'User should not be new any more.'); 29 | assert.equal(vm.user.password, '', 'User\'s password property should be cleared after user gets created/updated.'); 30 | 31 | done(); 32 | }, function() { 33 | done(); 34 | }); 35 | }); 36 | 37 | QUnit.test('saveUser without password fails', function(assert) { 38 | var done = assert.async(); 39 | 40 | var vm = new ViewModel(); 41 | 42 | 43 | vm.user.email = 'test@bitovi.com'; 44 | assert.expect(2); 45 | 46 | vm.saveUser(); 47 | vm.savePromise.then(function(resp, type){ 48 | done(); 49 | }, function(resp) { 50 | assert.equal(resp.statusText, 'error', 'fail creation without password'); 51 | assert.equal(resp.status, 400, 'rejected'); 52 | done(); 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /public/components/user/details/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/components/user/list/list.html: -------------------------------------------------------------------------------- 1 | 12 | 60 | -------------------------------------------------------------------------------- /public/components/user/list/list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {Module} bitballs/components/user/list 3 | * @parent bitballs.components 4 | * 5 | * @description Provides a custom element that allows an admin user 6 | * to view the list of registered users, see whether they have 7 | * verified their email addresses, and change their admin status. 8 | * 9 | * @signature `` 10 | * Creates the user list. 11 | * 12 | * @param {bitballs/model/session} session The session object. Contains information 13 | * about the logged in user. If the logged in user is not an administrator, they 14 | * will not be able to view the user list. This also allows the component to prevent 15 | * the logged in user from removing themseves as an administrator. 16 | * 17 | * @body 18 | * 19 | * To create a `` element, pass the [bitballs/model/session] like: 20 | * 21 | * ``` 22 | * 25 | * ``` 26 | * 27 | * ## Example 28 | * 29 | * @demo public/components/user/list/list.html 30 | */ 31 | import { Component, DefineMap } from "can"; 32 | import './list.less'; 33 | import view from './list.stache'; 34 | import User from "bitballs/models/user"; 35 | import Session from "bitballs/models/session"; 36 | 37 | /** 38 | * @constructor bitballs/components/user/list.ViewModel ViewModel 39 | * @parent bitballs/components/user/list 40 | * 41 | * @description A `` component's ViewModel. 42 | */ 43 | 44 | export const ViewModel = DefineMap.extend({ 45 | /** 46 | * @property {bitballs/models/session} session 47 | * The session object if a user is logged in. The user must be an admin to view the user list. 48 | */ 49 | session: Session, 50 | /** 51 | * @property {can-list} 52 | * 53 | * Provides list of users, like: 54 | * 55 | * {data: [{ 56 | * "id": Int, 57 | * "email": String, 58 | * "isAdmin": Boolean, 59 | * "verified": Boolean 60 | * }, ...]} 61 | * 62 | */ 63 | users: { 64 | get: function(list) { 65 | if (list) { 66 | return list; 67 | } 68 | return User.getList({}); 69 | } 70 | }, 71 | /** 72 | * @function 73 | * 74 | * Sets the user's admin status. 75 | * 76 | * @param {bitballs/models/user} user The user object that will be set or unset as an admin. 77 | * @param {Boolean} isAdmin Whether the user should be set as an admin. 78 | * 79 | * @return {Promise 3 | @signature `` 4 | 5 | @body 6 | 7 | ## Users 8 | -------------------------------------------------------------------------------- /public/components/user/list/list.stache: -------------------------------------------------------------------------------- 1 | {{# if(session.isAdmin() ) }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{# if(users.isPending) }} 13 | 14 | 15 | 16 | {{/ if }} 17 | {{# if(users.isResolved) }} 18 | {{# each(users.value) }} 19 | 20 | 21 | 22 | 23 | 38 | 39 | {{/ each }} 40 | {{/ if }} 41 | 42 |
IdEmailVerifiedIs Admin
LOADING...
{{ id }}{{ email }}{{ verified }} 24 | {{# is(id, ../session.user.id) }} 25 | {{ isAdmin }} 26 | {{ else }} 27 | {{# if(../session.user.isAdmin) }} 28 | 33 | {{ else }} 34 | {{ isAdmin }} 35 | {{/ if }} 36 | {{/ is }} 37 |
43 | {{/ if }} 44 | -------------------------------------------------------------------------------- /public/components/user/list/list_test.js: -------------------------------------------------------------------------------- 1 | import QUnit from 'steal-qunit'; 2 | 3 | // ViewModel unit tests 4 | QUnit.module('bitballs/components/user/list'); 5 | 6 | QUnit.test('Has message', function(){ 7 | QUnit.ok(true, 'Has a test'); 8 | }); 9 | -------------------------------------------------------------------------------- /public/components/user/list/test.html: -------------------------------------------------------------------------------- 1 | bitballs/components/user/list 2 | 3 |
-------------------------------------------------------------------------------- /public/components/user/test.js: -------------------------------------------------------------------------------- 1 | import './details/details_test'; 2 | import './list/list_test'; -------------------------------------------------------------------------------- /public/dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/img/bitballs-logo-02.svg: -------------------------------------------------------------------------------- 1 | bitballs-logo -------------------------------------------------------------------------------- /public/index.stache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | {{# if(pageComponent.isResolved) }} 16 | {{pageComponent.value}} 17 | {{ else }} 18 | Loading... 19 | {{/ if }} 20 |
21 | 22 | {{# is(env.NODE_ENV, "production") }} 23 | 24 | {{ else }} 25 | 27 | {{/ is }} 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/inserted-removed.js: -------------------------------------------------------------------------------- 1 | import domEvents from "can-dom-events"; 2 | import domMutateDomEvents from "can-dom-mutate/dom-events"; 3 | 4 | domEvents.addEvent(domMutateDomEvents.inserted); 5 | domEvents.addEvent(domMutateDomEvents.removed); 6 | -------------------------------------------------------------------------------- /public/is-dev.js: -------------------------------------------------------------------------------- 1 | import steal from "@steal"; 2 | 3 | // The slim loader doesn't include a isEnv, so that means it's prod. 4 | export default !(!steal.isEnv || steal.isEnv("production")); 5 | -------------------------------------------------------------------------------- /public/models/bookshelf-service.js: -------------------------------------------------------------------------------- 1 | import { key } from "can"; 2 | 3 | const bookshelfService = { 4 | toQuery(params) { 5 | return key.transform(params, { 6 | where: "filter", 7 | orderBy: "sort" 8 | }); 9 | }, 10 | toParams(query){ 11 | return key.transform(query, { 12 | filter: "where", 13 | sort: "orderBy" 14 | }); 15 | } 16 | }; 17 | 18 | export default bookshelfService; 19 | -------------------------------------------------------------------------------- /public/models/fixtures/fixtures.js: -------------------------------------------------------------------------------- 1 | import "./games"; 2 | import "./players"; 3 | import "./tournaments"; 4 | import "./users"; 5 | import "./stats"; 6 | -------------------------------------------------------------------------------- /public/models/fixtures/games.js: -------------------------------------------------------------------------------- 1 | import { fixture } from "can"; 2 | 3 | export const games = { 4 | id: 1, 5 | videoUrl: "AEUULIs_UWE", 6 | homeTeamId: 1, 7 | round: "Round 1", 8 | court: "1", 9 | tournament: { 10 | id: 1, 11 | date: "2012-01-01" 12 | }, 13 | finalScore: { 14 | home: 22, 15 | away: 20 16 | }, 17 | currentScore: { 18 | home: 0, 19 | away: 0 20 | }, 21 | homeTeam: { 22 | id: 1, 23 | player1Id:1, 24 | name: "Solid as A Rock", 25 | color: "I Blue Myself", 26 | player1:{ 27 | id: 1, 28 | name: "George Bluth", 29 | weight: 180, 30 | height: 60, 31 | birthday: "14/11/1960", 32 | profile: "This is a player", 33 | startRank: "" 34 | }, 35 | player2Id:2, 36 | player2:{ 37 | id: 2, 38 | name: "Micheal Bluth", 39 | weight: 180, 40 | height: 60, 41 | birthday: "14/11/1960", 42 | profile: "This is a player", 43 | startRank: "" 44 | }, 45 | player3Id:3, 46 | player3:{ 47 | id: 3, 48 | name: "Lucille Bluth", 49 | weight: 180, 50 | height: 60, 51 | birthday: "14/11/1960", 52 | profile: "This is a player", 53 | startRank: "" 54 | }, 55 | player4Id:4, 56 | player4:{ 57 | id: 4, 58 | name: "Oscar Bluth", 59 | weight: 180, 60 | height: 60, 61 | birthday: "14/11/1960", 62 | profile: "This is a player", 63 | startRank: "" 64 | } 65 | }, 66 | awayTeamId: 2, 67 | awayTeam: { 68 | id: 2, 69 | player1Id:5, 70 | name: "Bob Loblaw Balls, Y'all", 71 | color: "Tobias's Favorite Shade of Pink", 72 | player1:{ 73 | id: 5, 74 | name: "Lucille Two", 75 | weight: 180, 76 | height: 60, 77 | birthday: "14/11/1960", 78 | profile: "This is a player", 79 | startRank: "" 80 | }, 81 | player2Id:6, 82 | player2:{ 83 | id: 6, 84 | name: "Anne", 85 | weight: 180, 86 | height: 60, 87 | birthday: "14/11/1960", 88 | profile: "This is a player", 89 | startRank: "" 90 | }, 91 | player3Id:7, 92 | player3:{ 93 | id: 7, 94 | name: "Bob Loblaw", 95 | weight: 180, 96 | height: 60, 97 | birthday: "14/11/1960", 98 | profile: "This is a player", 99 | startRank: "" 100 | }, 101 | player4Id:8, 102 | player4:{ 103 | id: 8, 104 | name: "Steve Holt", 105 | weight: 180, 106 | height: 60, 107 | birthday: "14/11/1960", 108 | profile: "This is a player", 109 | startRank: "" 110 | } 111 | }, 112 | stats: [{ 113 | id: 1, 114 | type: "1P", 115 | playerId: 4, 116 | time: 20 117 | }, 118 | { 119 | id: 2, 120 | type: "2P", 121 | playerId: 4, 122 | time: 40 123 | }, 124 | { 125 | id: 3, 126 | type: "1P", 127 | playerId: 5, 128 | time: 60 129 | }, 130 | { 131 | id: 4, 132 | type: "1P", 133 | playerId: 6, 134 | time: 80 135 | }, 136 | { 137 | id: 5, 138 | type: "1P", 139 | playerId: 7, 140 | time: 100 141 | }, 142 | { 143 | id: 6, 144 | type: "2P", 145 | playerId: 8, 146 | time: 120 147 | }] 148 | }; 149 | 150 | export const defineFixtures = function () { 151 | 152 | fixture('/services/games', function () { 153 | return { 154 | data: [games] 155 | }; 156 | }); 157 | 158 | fixture("/services/games/{id}", function(request, response) { 159 | if (request.data.id === "1" || request.data.id === 1) { 160 | response(games); 161 | } 162 | }); 163 | 164 | fixture('GET /services/stats', function () { 165 | return { data: games.stats }; 166 | }); 167 | 168 | fixture('DELETE /services/stats/{id}', function () { 169 | return {}; 170 | }); 171 | }; 172 | 173 | defineFixtures(); 174 | 175 | export default defineFixtures; 176 | -------------------------------------------------------------------------------- /public/models/fixtures/players.js: -------------------------------------------------------------------------------- 1 | import { fixture } from 'can'; 2 | 3 | export const players = { 4 | data: [{ 5 | id: 1, 6 | name: 'Test Player', 7 | weight: 200, 8 | height: 71, 9 | birthday: '1980-01-01', 10 | profile:null, 11 | startRank:null 12 | }] 13 | }; 14 | 15 | export const defineFixtures = function() { 16 | 17 | fixture('GET /services/players/{id}', function(req) { 18 | var data; 19 | players.data.forEach(function(player){ 20 | if (player.id === parseInt(req.data.id, 10)) { 21 | data = player; 22 | return true; 23 | } 24 | }); 25 | return data; 26 | }); 27 | 28 | fixture('GET /services/players', function(req) { 29 | return players; 30 | }); 31 | 32 | fixture('POST /services/players', function(request, response){ 33 | if(!request.data.name){ 34 | response(400, '{type: "Bad Request", message: "Can not create a player without a name"}'); 35 | }else{ 36 | response({ 37 | "id":1 38 | }); 39 | } 40 | }); 41 | 42 | fixture('PUT /services/players/{id}', function(req) { 43 | req.data.id = parseInt(req.data.id, 10); 44 | return req.data; 45 | }); 46 | }; 47 | 48 | defineFixtures(); 49 | 50 | export default defineFixtures; 51 | -------------------------------------------------------------------------------- /public/models/fixtures/stats.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | {"id":1,"gameId":1,"playerId":1,"type":"1P","time":15,"value":null}, 3 | {"id":2,"gameId":1,"playerId":7,"type":"1PA","time":32,"value":null}, 4 | {"id":3,"gameId":1,"playerId":3,"type":"1PA","time":43,"value":null}, 5 | {"id":4,"gameId":1,"playerId":7,"type":"1PA","time":49,"value":null}, 6 | {"id":5,"gameId":1,"playerId":2,"type":"1PA","time":54,"value":null}, 7 | {"id":6,"gameId":1,"playerId":1,"type":"1PA","time":84,"value":null}, 8 | {"id":7,"gameId":1,"playerId":3,"type":"To","time":97,"value":null}, 9 | {"id":8,"gameId":1,"playerId":6,"type":"1P","time":106,"value":null}, 10 | {"id":9,"gameId":1,"playerId":1,"type":"2PA","time":147,"value":null}, 11 | {"id":10,"gameId":1,"playerId":6,"type":"1PA","time":157,"value":null}, 12 | {"id":11,"gameId":1,"playerId":1,"type":"2PA","time":164,"value":null}, 13 | {"id":12,"gameId":1,"playerId":7,"type":"1P","time":172,"value":null}, 14 | {"id":13,"gameId":1,"playerId":2,"type":"1PA","time":203,"value":null}, 15 | {"id":14,"gameId":1,"playerId":6,"type":"1PA","time":216,"value":null}, 16 | {"id":15,"gameId":1,"playerId":1,"type":"1PA","time":226,"value":null}, 17 | {"id":16,"gameId":1,"playerId":6,"type":"1PA","time":230,"value":null}, 18 | {"id":17,"gameId":1,"playerId":1,"type":"2PA","time":240,"value":null}, 19 | {"id":18,"gameId":1,"playerId":8,"type":"2PA","time":260,"value":null}, 20 | {"id":19,"gameId":1,"playerId":6,"type":"1P","time":282,"value":null}, 21 | {"id":20,"gameId":1,"playerId":5,"type":"1P","time":307,"value":null}, 22 | {"id":21,"gameId":1,"playerId":5,"type":"1PA","time":335,"value":null}, 23 | {"id":22,"gameId":1,"playerId":2,"type":"1PA","time":355,"value":null}, 24 | {"id":23,"gameId":1,"playerId":6,"type":"1PA","time":365,"value":null}, 25 | {"id":24,"gameId":1,"playerId":5,"type":"Stl","time":392,"value":null}, 26 | {"id":25,"gameId":1,"playerId":5,"type":"1P","time":407,"value":null}, 27 | {"id":26,"gameId":1,"playerId":6,"type":"1P","time":432,"value":null}, 28 | {"id":27,"gameId":1,"playerId":3,"type":"2P","time":449,"value":null}, 29 | {"id":28,"gameId":1,"playerId":7,"type":"1PA","time":469,"value":null}, 30 | {"id":29,"gameId":1,"playerId":1,"type":"1PA","time":481,"value":null}, 31 | {"id":30,"gameId":1,"playerId":6,"type":"1P","time":495,"value":null}, 32 | {"id":31,"gameId":1,"playerId":5,"type":"1PA","time":510,"value":null}, 33 | {"id":32,"gameId":1,"playerId":5,"type":"1P","time":513,"value":null}, 34 | {"id":33,"gameId":1,"playerId":6,"type":"1PA","time":621,"value":null}, 35 | {"id":34,"gameId":1,"playerId":1,"type":"2P","time":629,"value":null}, 36 | {"id":35,"gameId":1,"playerId":6,"type":"1P","time":652,"value":null}, 37 | {"id":36,"gameId":1,"playerId":1,"type":"2P","time":671,"value":null}, 38 | {"id":37,"gameId":1,"playerId":3,"type":"1PA","time":723,"value":null}, 39 | {"id":38,"gameId":1,"playerId":7,"type":"1P","time":740,"value":null}, 40 | {"id":39,"gameId":1,"playerId":5,"type":"2P","time":766,"value":null}, 41 | {"id":40,"gameId":1,"playerId":6,"type":"2P","time":784,"value":null}, 42 | {"id":41,"gameId":1,"playerId":6,"type":null,"time":814,"value":null}, 43 | {"id":42,"gameId":1,"playerId":1,"type":"2PA","time":821,"value":null}, 44 | {"id":43,"gameId":1,"playerId":7,"type":"1PA","time":854,"value":null}, 45 | {"id":44,"gameId":1,"playerId":5,"type":"1PA","time":860,"value":null}, 46 | {"id":45,"gameId":1,"playerId":7,"type":"1PA","time":877,"value":null}, 47 | {"id":46,"gameId":1,"playerId":5,"type":"2PA","time":883,"value":null}, 48 | {"id":47,"gameId":1,"playerId":7,"type":"1PA","time":899,"value":null}, 49 | {"id":48,"gameId":1,"playerId":3,"type":"1PA","time":908,"value":null}, 50 | {"id":49,"gameId":1,"playerId":6,"type":"2PA","time":912,"value":null}, 51 | {"id":50,"gameId":1,"playerId":6,"type":"1P","time":918,"value":null}, 52 | {"id":51,"gameId":1,"playerId":6,"type":"1P","time":958,"value":null}, 53 | {"id":52,"gameId":1,"playerId":1,"type":"1PA","time":1018,"value":null}, 54 | {"id":53,"gameId":1,"playerId":3,"type":"1P","time":1066,"value":null}, 55 | {"id":54,"gameId":1,"playerId":6,"type":"2PA","time":1131,"value":null}, 56 | {"id":55,"gameId":1,"playerId":6,"type":"1P","time":1149,"value":null}, 57 | {"id":56,"gameId":1,"playerId":5,"type":"1PA","time":1162,"value":null}, 58 | {"id":57,"gameId":1,"playerId":6,"type":"1PA","time":1169,"value":null}, 59 | {"id":58,"gameId":1,"playerId":3,"type":"1PA","time":1175,"value":null}, 60 | {"id":59,"gameId":1,"playerId":1,"type":"2PA","time":1186,"value":null}, 61 | {"id":60,"gameId":1,"playerId":1,"type":"2PA","time":1202,"value":null}, 62 | {"id":61,"gameId":1,"playerId":6,"type":"1PA","time":1207,"value":null}, 63 | {"id":62,"gameId":1,"playerId":5,"type":"1P","time":1220,"value":null}, 64 | {"id":63,"gameId":1,"playerId":6,"type":"1P","time":1246,"value":null}, 65 | {"id":64,"gameId":5,"playerId":8,"type":"1P","time":189,"value":null}, 66 | {"id":65,"gameId":9,"playerId":40,"type":"2PA","time":9,"value":null}, 67 | {"id":66,"gameId":9,"playerId":40,"type":"1P","time":12,"value":null}, 68 | {"id":67,"gameId":9,"playerId":40,"type":"ORB","time":10,"value":null}, 69 | {"id":68,"gameId":9,"playerId":42,"type":"1PA","time":13,"value":null}, 70 | {"id":69,"gameId":9,"playerId":35,"type":"2P","time":229,"value":null}, 71 | {"id":71,"gameId":9,"playerId":19,"type":"1PA","time":24,"value":null}, 72 | {"id":72,"gameId":9,"playerId":35,"type":"ORB","time":25,"value":null}, 73 | {"id":73,"gameId":9,"playerId":35,"type":"1P","time":27,"value":null}, 74 | {"id":75,"gameId":9,"playerId":41,"type":"2PA","time":53,"value":null}, 75 | {"id":77,"gameId":9,"playerId":35,"type":"2P","time":79,"value":null}, 76 | {"id":78,"gameId":9,"playerId":41,"type":"2P","time":90,"value":null}, 77 | {"id":79,"gameId":9,"playerId":23,"type":"2P","time":109,"value":null} 78 | ]; -------------------------------------------------------------------------------- /public/models/fixtures/teams.js: -------------------------------------------------------------------------------- 1 | import { fixture } from "can"; 2 | 3 | export const teams = { 4 | data: [{ 5 | id: 1, 6 | name: 'Three Dog Night', 7 | color: 'Red', 8 | player1Id: 1 9 | }] 10 | }; 11 | 12 | export const defineFixtures = function () { 13 | fixture('services/teams', function () { 14 | return teams; 15 | }); 16 | 17 | fixture({method: "DELETE", url: 'services/teams/{id}'}, function() { 18 | return []; 19 | }); 20 | }; 21 | 22 | defineFixtures(); 23 | 24 | export default defineFixtures; 25 | -------------------------------------------------------------------------------- /public/models/fixtures/tournaments.js: -------------------------------------------------------------------------------- 1 | import { assign, fixture } from "can"; 2 | import $ from 'jquery'; 3 | 4 | export const tournaments = { 5 | data: [ 6 | { 7 | date: new Date("Fri Sep 04 2015 07:42:58 GMT-0500 (CDT)"), 8 | id: 2, 9 | tournamentId: 1, 10 | name: "EBaller Virus", 11 | color: "Yellow", 12 | player1Id: 1, 13 | player2Id: 2, 14 | player3Id: 3, 15 | player4Id: 5 16 | } 17 | ] 18 | }; 19 | 20 | export const defineFixtures = function () { 21 | fixture('POST /services/tournaments', function (req) { 22 | var data = assign({}, req.data); 23 | assign(data, { 24 | date: new Date(req.data.date), 25 | id: tournaments.data[tournaments.data.length - 1].id + 1 26 | }); 27 | 28 | tournaments.data.push(data); 29 | return data; 30 | }); 31 | 32 | fixture('POST /services/tournaments', function(req, response){ 33 | if(!req.data.date){ 34 | response(400, '{type: "Bad Request", message: "Can not create a tournament without a date"}'); 35 | } else { 36 | response({ 37 | id: 3 38 | }); 39 | } 40 | }); 41 | 42 | fixture('GET /services/tournaments', function (req) { 43 | return tournaments; 44 | }); 45 | 46 | fixture('GET /services/tournaments/{id}', function (req) { 47 | var data; 48 | $.each(tournaments.data, function (i, tourney) { 49 | if (tourney.id === parseInt(req.data.id, 10)) { 50 | data = tourney; 51 | return false; 52 | } 53 | }); 54 | return data; 55 | }); 56 | }; 57 | 58 | defineFixtures(); 59 | 60 | export default defineFixtures; 61 | -------------------------------------------------------------------------------- /public/models/fixtures/users.js: -------------------------------------------------------------------------------- 1 | import { fixture } from "can"; 2 | 3 | fixture("POST /services/users", function(request, response){ 4 | console.log('[fixture] request', request); 5 | 6 | if(!request.data.password){ 7 | response(400, '{type: "Bad Request", message: "Can not create a user without a password"}'); 8 | }else{ 9 | response({ 10 | id: 123, 11 | email: request.data.email 12 | }); 13 | } 14 | }); 15 | 16 | fixture("PUT /services/users/{id}", function(request, response){ 17 | if(!request.data.password){ 18 | response(400, '{type: "Bad Request", message: "Can not create a user without a password"}'); 19 | }else{ 20 | response({ 21 | id: request.data.id, 22 | email: request.data.email 23 | }); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /public/models/game_test.js: -------------------------------------------------------------------------------- 1 | import QUnit from "steal-qunit"; 2 | import Game from './game'; 3 | 4 | QUnit.module('bitballs/models/game', { 5 | setup: function () { 6 | this.game = new Game(); 7 | } 8 | }); 9 | 10 | QUnit.test('Video url can be a YouTube url or key', function () { 11 | var game = this.game; 12 | var videoKey = '0zM3nApSvMg'; 13 | 14 | var sampleUrls = [ 15 | '0zM3nApSvMg', 16 | 'http://www.youtube.com/v/0zM3nApSvMg?fs=1&hl=en_US&rel=0', 17 | 'http://www.youtube.com/embed/0zM3nApSvMg?rel=0', 18 | 'http://www.youtube.com/watch?v=0zM3nApSvMg&feature=feedrec_grec_index', 19 | 'http://www.youtube.com/watch?v=0zM3nApSvMg', 20 | 'http://youtu.be/0zM3nApSvMg', 21 | 'http://www.youtube.com/watch?v=0zM3nApSvMg#t=0m10s', 22 | 'http://www.youtube.com/user/IngridMichaelsonVEVO#p/a/u/1/0zM3nApSvMg', 23 | 'http://youtu.be/0zM3nApSvMg', 24 | 'http://www.youtube.com/embed/0zM3nApSvMg', 25 | 'http://www.youtube.com/v/0zM3nApSvMg', 26 | 'http://www.youtube.com/e/0zM3nApSvMg', 27 | 'http://www.youtube.com/watch?v=0zM3nApSvMg', 28 | 'http://www.youtube.com/?v=0zM3nApSvMg', 29 | 'http://www.youtube.com/watch?feature=player_embedded&v=0zM3nApSvMg', 30 | 'http://www.youtube.com/?feature=player_embedded&v=0zM3nApSvMg', 31 | 'http://www.youtube.com/user/IngridMichaelsonVEVO#p/u/11/0zM3nApSvMg', 32 | 'http://www.youtube-nocookie.com/v/0zM3nApSvMg?version=3&hl=en_US&rel=0' 33 | ]; 34 | sampleUrls.forEach(function(url){ 35 | game.attr('videoUrl', url); 36 | QUnit.equal(game.attr('videoUrl'), videoKey, 'Video key was extracted from input'); 37 | }); 38 | }); 39 | 40 | QUnit.test('Rounds are not available if all their courts are assigned games', function () { 41 | var gameList = new Game.List(); 42 | 43 | Game.courtNames.forEach(function (courtName) { 44 | gameList.push({ 45 | round: Game.roundNames[0], 46 | court: courtName 47 | }); 48 | }); 49 | 50 | QUnit.deepEqual(gameList.getAvailableRounds(), Game.roundNames.slice(1), 51 | 'The first round is not available'); 52 | }); 53 | 54 | QUnit.test('Courts are not available if they are assigned games', function () { 55 | var gameList = new Game.List([{ 56 | round: Game.roundNames[0], 57 | court: Game.courtNames[0] 58 | }]); 59 | 60 | QUnit.deepEqual(gameList.getAvailableCourts(Game.roundNames[0]), Game.courtNames.slice(1), 61 | 'The first court is not available'); 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /public/models/player.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {can-map} bitballs/models/player Player 3 | * @parent bitballs.clientModels 4 | * 5 | * @group bitballs/models/player.properties 0 properties 6 | */ 7 | import { superModel, QueryLogic, DefineMap, DefineList, defineBackup } from "can"; 8 | import moment from "moment"; 9 | import bookshelfService from "./bookshelf-service"; 10 | 11 | var Player = DefineMap.extend('Player', { 12 | /** 13 | * @property {Number} bitballs/models/player.properties.id id 14 | * @parent bitballs/models/player.properties 15 | * 16 | * A unique identifier. 17 | **/ 18 | id: {type: 'number', identity: true}, 19 | 20 | /** 21 | * @property {String} bitballs/models/player.properties.birthday birthday 22 | * @parent bitballs/models/player.properties 23 | * 24 | * The player's date of birth. Formatted as `YYYY-MM-DD`. 25 | **/ 26 | birthday: 'any', 27 | /** 28 | * @property {String} bitballs/models/player.properties.name name 29 | * @parent bitballs/models/player.properties 30 | * 31 | * The name of the player. 32 | **/ 33 | name: 'string', 34 | /** 35 | * @property {Number} bitballs/models/player.properties.weight weight 36 | * @parent bitballs/models/player.properties 37 | * 38 | * The weight of a player in pounds. 39 | **/ 40 | weight: 'number', 41 | /** 42 | * @property {Number} bitballs/models/player.properties.height height 43 | * @parent bitballs/models/player.properties 44 | * 45 | * The height of a player in inches. 46 | **/ 47 | height: 'number', 48 | 49 | // flag set by the api when a player is destroyed 50 | _destroyed: 'boolean', 51 | 52 | profile: 'any', 53 | 54 | startRank: 'any', 55 | 56 | /** 57 | * @function 58 | * 59 | * Backs up the model's properties on instantiation. 60 | **/ 61 | init: function () { 62 | this.backup(); 63 | }, 64 | /** 65 | * @property {Date|null} bitballs/models/player.properties.jsBirthday jsBirthday 66 | * @parent bitballs/models/player.properties 67 | * 68 | * The [bitballs/models/player.properties.birthday birthday] property 69 | * represented as a JavaScript object. 70 | **/ 71 | get jsBirthday() { 72 | var date = this.birthday; 73 | return date ? new Date(date) : null; 74 | }, 75 | /** 76 | * @property {String} bitballs/models/player.properties.birthDate birthDate 77 | * @parent bitballs/models/player.properties 78 | * 79 | * The [bitballs/models/player.properties.birthday birthday] property 80 | * formatted as `YYYY-MM-DD`. 81 | **/ 82 | get birthDate() { 83 | var date = this.birthday; 84 | return date ? moment(date).format('YYYY-MM-DD') : ""; 85 | }, 86 | set birthDate(value) { 87 | this.birthday = value; 88 | }, 89 | /** 90 | * @property {Number} bitballs/models/player.properties.age age 91 | * @parent bitballs/models/player.properties 92 | * 93 | * The number of full years since the date of the 94 | * [bitballs/models/player.properties.jsBirthday jsBirthday] property. 95 | **/ 96 | get age() { 97 | var birthDate = this.jsBirthday; 98 | if(birthDate) { 99 | var today = new Date(); 100 | var age = today.getFullYear() - birthDate.getFullYear(); 101 | var m = today.getMonth() - birthDate.getMonth(); 102 | if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { 103 | age--; 104 | } 105 | return age; 106 | } 107 | } 108 | }); 109 | defineBackup(Player); 110 | 111 | /** 112 | * @constructor {can-list} bitballs/models/player.static.List List 113 | * @parent bitballs/models/player.static 114 | */ 115 | Player.List = DefineList.extend('PlayerList', 116 | /** @prototype **/ 117 | { 118 | "#": Player, 119 | /** 120 | * @property {Object} 121 | * 122 | * A map of player ids to [bitballs/models/player] models. 123 | **/ 124 | get idMap() { 125 | var map = {}; 126 | this.forEach(function(player){ 127 | map[player.id] = player; 128 | }); 129 | 130 | return map; 131 | }, 132 | 133 | /** 134 | * @function 135 | * 136 | * Returns a Player in the list of players given its id. 137 | * 138 | * @param {Number} id 139 | * @return {bitballs/models/player|undefined} The player if it exists. 140 | */ 141 | getById: function(id){ 142 | return this.idMap[id]; 143 | } 144 | }); 145 | 146 | Player.connection = superModel({ 147 | Map: Player, 148 | List: Player.List, 149 | url: { 150 | resource: "/services/players", 151 | contentType: 'application/x-www-form-urlencoded' 152 | }, 153 | name: "player", 154 | queryLogic: new QueryLogic(Player, bookshelfService), 155 | updateInstanceWithAssignDeep: true 156 | }); 157 | 158 | 159 | 160 | 161 | export default Player; 162 | -------------------------------------------------------------------------------- /public/models/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {can-map} bitballs/models/session Session 3 | * @parent bitballs.clientModels 4 | * 5 | * @group bitballs/models/session.properties 0 properties 6 | */ 7 | 8 | import { 9 | restModel, 10 | QueryLogic, 11 | DefineMap, 12 | DefineList 13 | } from "can"; 14 | import bookshelfService from "./bookshelf-service"; 15 | import $ from "jquery"; 16 | import User from "./user"; 17 | 18 | 19 | var Session = DefineMap.extend('Session', { 20 | /** 21 | * @property {bitballs/models/user} bitballs/models/session.properties.user user 22 | * @parent bitballs/models/session.properties 23 | * 24 | * The [bitballs/models/user] model this session represents. 25 | **/ 26 | user: User, 27 | /** 28 | * @function 29 | * 30 | * Identifies whether or not the [bitballs/models/session.properties.user] 31 | * property is an administrator. 32 | * 33 | * @return {Boolean} 34 | **/ 35 | isAdmin: function(){ 36 | return this.user && this.user.isAdmin; 37 | } 38 | }); 39 | 40 | /** 41 | * @constructor {can-list} bitballs/models/session.static.List List 42 | * @parent bitballs/models/session.static 43 | */ 44 | Session.List = DefineList.extend('SessionList', {"#": Session}); 45 | 46 | 47 | Session.connection = restModel({ 48 | ajax: $.ajax, 49 | Map: Session, 50 | List: Session.List, 51 | //name: "session", 52 | url: { 53 | getData: "/services/session", 54 | createData: "/services/session", 55 | destroyData: "/services/session", 56 | contentType: "application/x-www-form-urlencoded" 57 | }, 58 | queryLogic: new QueryLogic(Session, bookshelfService), 59 | updateInstanceWithAssignDeep: true 60 | }); 61 | 62 | export default Session; 63 | -------------------------------------------------------------------------------- /public/models/stat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {can-map} bitballs/models/stat Stat 3 | * @parent bitballs.clientModels 4 | * 5 | * @group bitballs/models/stat.properties 0 properties 6 | * 7 | * A [can.Map](https://canjs.com/docs/can.Map.html) that's connected to the [services/stats] with 8 | * all of [can-connect/can/super-map](https://connect.canjs.com/doc/can-connect%7Ccan%7Csuper-map.html)'s 9 | * behaviors. 10 | * 11 | * @body 12 | * 13 | * ## Use 14 | * 15 | * Use the `Stat` model to CRUD stats on the server. Use the CRUD methods `getList`, `save`, and `destroy` added to 16 | * `Stat` by the [can-connect/can/map](https://connect.canjs.com/doc/can-connect%7Ccan%7Cmap.html) behavior. 17 | * 18 | * 19 | * ``` 20 | * var Stat = require("bitballs/models/stat"); 21 | * Stat.getList({where: {gameId: 5 }}).then(function(stats){ ... }); 22 | * new Stat({gameId: 6, playerId: 15, type: "1P", time: 60}).save() 23 | * ``` 24 | */ 25 | import { DefineMap, DefineList, QueryLogic, realtimeRestModel } from "can"; 26 | import bookshelfService from "./bookshelf-service"; 27 | import Player from "bitballs/models/player"; 28 | 29 | var Stat = DefineMap.extend('Stat', 30 | { 31 | /** 32 | * @property {Array<{name: String}>} statTypes 33 | * 34 | * Array of statType objects. Each object has a name property which 35 | * has the short name of the stat. Ex: `{name: "1P"}`. 36 | */ 37 | statTypes: [ 38 | { name: "1P"}, 39 | { name: "1PA"}, 40 | { name: "2P"}, 41 | { name: "2PA"}, 42 | { name: "ORB"}, 43 | { name: "DRB"}, 44 | { name: "Ast"}, 45 | { name: "Stl"}, 46 | { name: "Blk"}, 47 | { name: "To"} 48 | ] 49 | }, 50 | { 51 | /** 52 | * @property {Number} bitballs/models/stat.properties.id id 53 | * @parent bitballs/models/stat.properties 54 | * A unique identifier. 55 | **/ 56 | id: {type: 'number', identity: true}, 57 | /** 58 | * @property {bitballs/models/player} bitballs/models/stat.properties.player player 59 | * @parent bitballs/models/player.properties 60 | * 61 | * Player related to the stats 62 | */ 63 | player: { 64 | Type: Player, 65 | serialize: false 66 | }, 67 | /** 68 | * @property {Number} bitballs/models/stat.properties.playerId playerId 69 | * @parent bitballs/models/player.properties 70 | * 71 | * Player id of the current stats 72 | */ 73 | playerId: 'number', 74 | /** 75 | * @property {Number} bitballs/models/stat.properties.gameId gameId 76 | * @parent bitballs/models/player.properties 77 | * 78 | * Game id of the current stat 79 | */ 80 | gameId: 'number', 81 | /** 82 | * @property {Any} bitballs/models/stat.properties.type type 83 | * @parent bitballs/models/player.properties 84 | * 85 | * Type of the stat 86 | */ 87 | type: 'any', 88 | /** 89 | * @property {Number} bitballs/models/stat.properties.time time 90 | * @parent bitballs/models/stat.properties 91 | * 92 | * The time of the stat, rounded to the nearest integer. 93 | */ 94 | time: { 95 | set: function(newVal){ 96 | return Math.round(newVal); 97 | } 98 | }, 99 | 100 | default: 'any' 101 | }); 102 | 103 | 104 | /** 105 | * @property {can-define/list} bitballs/models/stat.static.List List 106 | * @parent bitballs/models/stat.static 107 | * 108 | * Methods on a List of stats. 109 | */ 110 | Stat.List = DefineList.extend('StatsList', { 111 | "#": Stat, 112 | get byPlayer() { 113 | let players = {}; 114 | 115 | this.forEach((stat) => { 116 | if (!players[stat.playerId]) { 117 | players[stat.playerId] = new Stat.List(); 118 | } 119 | 120 | players[stat.playerId].push(stat); 121 | }); 122 | 123 | return players; 124 | }, 125 | get players() { 126 | return Object.keys(this.byPlayer).map((id) => ({ id })); 127 | }, 128 | get byGame() { 129 | let games = {}; 130 | 131 | this.forEach((stat) => { 132 | if (!games[stat.gameId]) { 133 | games[stat.gameId] = new Stat.List(); 134 | } 135 | 136 | games[stat.gameId].push(stat); 137 | }); 138 | 139 | return games; 140 | }, 141 | get games() { 142 | return Object.keys(this.byGame).map((id) => ({ id })); 143 | }, 144 | get aggregated() { 145 | let aggregated = {}; 146 | 147 | this.forEach(({ type }) => { 148 | if (!aggregated[type]) { 149 | aggregated[type] = 0; 150 | } 151 | 152 | aggregated[type]++; 153 | }); 154 | 155 | let aggregatedStats = [ 156 | ...Stat.statTypes.map(({ name }) => ({ 157 | name, 158 | default: (aggregated[name] || 0).toFixed(0), 159 | })), 160 | { 161 | name: 'TP', 162 | default: (function() { 163 | let onePointers = aggregated['1P'] || 0; 164 | let twoPointers = aggregated['2P'] || 0; 165 | 166 | return (onePointers + twoPointers * 2).toFixed(0); 167 | })() 168 | }, 169 | { 170 | name: 'FG%', 171 | default: (function() { 172 | let onePointers = aggregated['1P'] || 0; 173 | let twoPointers = aggregated['2P'] || 0; 174 | let onePointAttempts = aggregated['1PA'] || 0; 175 | let twoPointAttempts = aggregated['2PA'] || 0; 176 | 177 | let successes = onePointers + twoPointers; 178 | let attempts = onePointAttempts + twoPointAttempts; 179 | let rate = successes / ( successes + attempts ); 180 | 181 | if (isNaN(rate)) { 182 | return '-'; 183 | } 184 | 185 | return (rate * 100).toFixed(0) + '%'; 186 | })() 187 | }, 188 | ]; 189 | return aggregatedStats; 190 | }, 191 | }); 192 | 193 | 194 | Stat.connection = realtimeRestModel({ 195 | Map: Stat, 196 | List: Stat.List, 197 | url: { 198 | resource: "/services/stats", 199 | contentType: "application/x-www-form-urlencoded" 200 | }, 201 | name: "stat", 202 | queryLogic: new QueryLogic(Stat, bookshelfService), 203 | updateInstanceWithAssignDeep: true 204 | }); 205 | 206 | 207 | export default Stat; 208 | -------------------------------------------------------------------------------- /public/models/stat_test.js: -------------------------------------------------------------------------------- 1 | import QUnit from "steal-qunit"; 2 | import Stat from './stat'; 3 | import stats from 'bitballs/models/fixtures/stats'; 4 | 5 | QUnit.module('bitballs/models/tournament', { 6 | setup: function () { 7 | } 8 | }); 9 | 10 | QUnit.test('It should return a list of aggregated stats', function () { 11 | let statList = new Stat.List(stats); 12 | let aggregatedStats = statList.aggregated; 13 | QUnit.deepEqual(aggregatedStats[0], {name: '1P', default: "20"}); 14 | QUnit.deepEqual(aggregatedStats[1], {name: '1PA', default: "30"}); 15 | QUnit.deepEqual(aggregatedStats[2], {name: '2P', default: "9"}); 16 | }); 17 | -------------------------------------------------------------------------------- /public/models/team.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {can-map} bitballs/models/team Team 3 | * @parent bitballs.clientModels 4 | * 5 | * @group bitballs/models/team.properties 0 properties 6 | */ 7 | import { DefineMap, DefineList, superModel, QueryLogic } from "can"; 8 | import bookshelfService from "./bookshelf-service"; 9 | import Player from "./player"; 10 | 11 | var Team = DefineMap.extend('Team', { 12 | /** 13 | * @property {Array} 14 | * A list of available team colors. 15 | **/ 16 | colors: ["Black","White","Red","Green","Blue","Yellow","Brown","Gray","Orange","Purple"] 17 | }, 18 | { 19 | /** 20 | * @property {Number} bitballs/models/team.properties.id id 21 | * @parent bitballs/models/team.properties 22 | * 23 | * A unique identifier. 24 | **/ 25 | id: {type: 'number', identity: true}, 26 | /** 27 | * @property {Number} bitballs/models/team.properties.tournamentId tournamentId 28 | * @parent bitballs/models/team.properties 29 | * 30 | * The `id` of [bitballs/models/tournament] that the team will be 31 | * associated with. 32 | **/ 33 | tournamentId: "number", 34 | /** 35 | * @property {bitballs/models/player} bitballs/models/team.properties.player1 player1 36 | * @parent bitballs/models/team.properties 37 | * 38 | * A reference to a [bitballs/models/player] model. 39 | **/ 40 | player1: Player, 41 | /** 42 | * @property {bitballs/models/player} bitballs/models/team.properties.player2 player2 43 | * @parent bitballs/models/team.properties 44 | * 45 | * A reference to a [bitballs/models/player] model. 46 | **/ 47 | player2: Player, 48 | /** 49 | * @property {bitballs/models/player} bitballs/models/team.properties.player3 player3 50 | * @parent bitballs/models/team.properties 51 | * 52 | * A reference to a [bitballs/models/player] model. 53 | **/ 54 | player3: Player, 55 | /** 56 | * @property {bitballs/models/player} bitballs/models/team.properties.player4 player4 57 | * @parent bitballs/models/team.properties 58 | * 59 | * A reference to a [bitballs/models/player] model. 60 | **/ 61 | player4: Player, 62 | /** 63 | * @property {String} bitballs/models/team.properties.name name 64 | * @parent bitballs/models/team.properties 65 | * 66 | * Name of the team 67 | **/ 68 | name: 'string', 69 | /** 70 | * @property {String} bitballs/models/team.properties.color color 71 | * @parent bitballs/models/team.properties 72 | * 73 | * Team color 74 | **/ 75 | color: 'string', 76 | /** 77 | * @property {Number} bitballs/models/team.properties.player1Id player1Id 78 | * @parent bitballs/models/team.properties 79 | * 80 | * id of the player 1. 81 | **/ 82 | player1Id: 'number', 83 | /** 84 | * @property {Number} bitballs/models/team.properties.player2Id player1Id 85 | * @parent bitballs/models/team.properties 86 | * 87 | * id of the player 2. 88 | **/ 89 | player2Id: 'number', 90 | /** 91 | * @property {Number} bitballs/models/team.properties.player3Id player1Id 92 | * @parent bitballs/models/team.properties 93 | * 94 | * id of the player 3. 95 | **/ 96 | player3Id: 'number', 97 | /** 98 | * @property {Number} bitballs/models/team.properties.player4Id player1Id 99 | * @parent bitballs/models/team.properties 100 | * 101 | * id of the player 4. 102 | **/ 103 | player4Id: 'number', 104 | /** 105 | * @property {bitballs/models/player.static.List} bitballs/models/team.properties.players players 106 | * @parent bitballs/models/team.properties 107 | * 108 | * A list made up of the [bitballs/models/player] models referenced 109 | * by properties [bitballs/models/team.properties.player1], 110 | * [bitballs/models/team.properties.player2], [bitballs/models/team.properties.player3], 111 | * and [bitballs/models/team.properties.player4]. 112 | **/ 113 | get players() { 114 | var players = [], 115 | self = this; 116 | ["player1","player2","player3","player4"].map(function(name){ 117 | if(self[name]) { 118 | players.push(self[name]); 119 | } 120 | }); 121 | return new Player.List(players); 122 | } 123 | }); 124 | /** 125 | * @constructor {can-list} bitballs/models/team.static.List List 126 | * @parent bitballs/models/team.static 127 | */ 128 | Team.List = DefineList.extend('TeamsList', 129 | /** @prototype **/ 130 | { 131 | "#": Team, 132 | /** 133 | * @property {Object} 134 | * 135 | * A map of team ids to [bitballs/models/team] models. 136 | **/ 137 | get idMap() { 138 | var map = {}; 139 | 140 | this.forEach(function(team){ 141 | map[team.id] = team; 142 | }); 143 | 144 | return map; 145 | }, 146 | /** 147 | * @function 148 | * 149 | * Iterates the list of the [bitballs/models/team] models and removes the 150 | * [bitballs/models/team] with the specified `id`. 151 | * 152 | * @param {Number} id 153 | **/ 154 | removeById: function(id){ 155 | var i = 0; 156 | while(i < this.length) { 157 | if(this[i].id === id) { 158 | this.splice(i, 1); 159 | } else { 160 | i++; 161 | } 162 | } 163 | }, 164 | /** 165 | * @function 166 | * Returns a Team in the list of teams given its id. 167 | * @param {Number} id 168 | * @return {bitballs/models/team|undefined} The team if it exists. 169 | */ 170 | getById: function(id){ 171 | return this.idMap[id]; 172 | } 173 | }); 174 | 175 | superModel({ 176 | Map: Team, 177 | List: Team.List, 178 | url: { 179 | resource: "/services/teams", 180 | contentType: "application/x-www-form-urlencoded" 181 | }, 182 | name: "team", 183 | queryLogic: new QueryLogic(Team, bookshelfService), 184 | updateInstanceWithAssignDeep: true 185 | }); 186 | 187 | export default Team; 188 | -------------------------------------------------------------------------------- /public/models/test.html: -------------------------------------------------------------------------------- 1 | Bitballs Model Tests 2 | 3 |
-------------------------------------------------------------------------------- /public/models/test.js: -------------------------------------------------------------------------------- 1 | import './fixtures/'; 2 | import './tournament_test'; 3 | import './stat_test'; 4 | -------------------------------------------------------------------------------- /public/models/tournament.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {can-map} bitballs/models/tournament Tournament 3 | * @parent bitballs.clientModels 4 | * 5 | * @group bitballs/models/tournament.properties 0 properties 6 | */ 7 | import { 8 | superModel, QueryLogic, DefineMap, DefineList 9 | } from "can"; 10 | import bookshelfService from "./bookshelf-service"; 11 | import moment from "moment"; 12 | 13 | 14 | var Tournament = DefineMap.extend('Tournament', { 15 | /** 16 | * @property {Number} bitballs/models/tournament.properties.id id 17 | * @parent bitballs/models/tournament.properties 18 | * 19 | * A unique identifier. 20 | **/ 21 | id: {type: 'number', identity: true}, 22 | /** 23 | * @property {String} bitballs/models/tournament.properties.date date 24 | * @parent bitballs/models/tournament.properties 25 | * 26 | * The date that the tournament is schedule to occur. 27 | **/ 28 | date: 'string', 29 | /** 30 | * @property {Date} bitballs/models/tournament.properties.jsDate jsDate 31 | * @parent bitballs/models/tournament.properties 32 | * 33 | * The [bitballs/models/tournament.properties.date] converted to a 34 | * JavaScript Date object. 35 | **/ 36 | get jsDate() { 37 | return moment(this.date).toDate() || null; 38 | }, 39 | /** 40 | * @property {Date} bitballs/models/tournament.properties.year year 41 | * @parent bitballs/models/tournament.properties 42 | * 43 | * The year referred to by [bitballs/models/tournament.properties.jsDate]. 44 | **/ 45 | get year() { 46 | var jsDate = this.jsDate; 47 | return jsDate ? jsDate.getFullYear() : null; 48 | }, 49 | /** 50 | * @property {Date} bitballs/models/tournament.properties.prettyDate prettyDate 51 | * @parent bitballs/models/tournament.properties 52 | * 53 | * A formatted output of [bitballs/models/tournament.properties.date]. 54 | **/ 55 | get prettyDate() { 56 | var date = new Date(this.date); 57 | return isNaN(date) ? null : date; 58 | } 59 | }); 60 | 61 | /** 62 | * @constructor {can-list} bitballs/models/tournament.static.List List 63 | * @parent bitballs/models/tournament.static 64 | */ 65 | Tournament.List = DefineList.extend('TournamentList', {"#": Tournament}); 66 | 67 | 68 | Tournament.connection = superModel({ 69 | Map: Tournament, 70 | List: Tournament.List, 71 | url: { 72 | resource: "/services/tournaments", 73 | contentType: "application/x-www-form-urlencoded" 74 | }, 75 | name: "tournament", 76 | queryLogic: new QueryLogic(Tournament, bookshelfService), 77 | updateInstanceWithAssignDeep: true 78 | }); 79 | 80 | export default Tournament; 81 | -------------------------------------------------------------------------------- /public/models/tournament_test.js: -------------------------------------------------------------------------------- 1 | import QUnit from "steal-qunit"; 2 | import Tournament from './tournament'; 3 | 4 | QUnit.module('bitballs/models/tournament', { 5 | setup: function () { 6 | this.tournament = new Tournament(); 7 | } 8 | }); 9 | 10 | QUnit.test('Year does not get improperly displayed based on time zone', function () { 11 | var tournament = this.tournament; 12 | tournament.date = '2016-01-01'; 13 | QUnit.equal(tournament.year, '2016'); 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /public/models/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {can-map} bitballs/models/user User 3 | * @parent bitballs.clientModels 4 | * 5 | * @group bitballs/models/user.static 0 static 6 | * 7 | * A [can.Map](https://canjs.com/docs/can.Map.html) that's connected to the [services/users] with 8 | * all of [can-connect/can/super-map](https://connect.canjs.com/doc/can-connect%7Ccan%7Csuper-map.html)'s 9 | * behaviors. 10 | * 11 | * @body 12 | * 13 | * ## Use 14 | * 15 | * Use the `User` model to CRUD users on the server. Use the CRUD methods `getList`, `save`, and `destroy` added to 16 | * `User` by the [can-connect/can/map](https://connect.canjs.com/doc/can-connect%7Ccan%7Cmap.html) behavior. 17 | * 18 | * 19 | * ``` 20 | * var User = require("bitballs/models/user"); 21 | * User.getList({where: {gameId: 5 }}).then(function(users){ ... }); 22 | * new User({gameId: 6, playerId: 15, type: "1P", time: 60}).save() 23 | * ``` 24 | */ 25 | import { superModel, QueryLogic, DefineMap, DefineList } from "can"; 26 | import bookshelfService from "./bookshelf-service"; 27 | 28 | 29 | var User = DefineMap.extend('User', { 30 | /** 31 | * @property {Number} bitballs/models/user.properties.id id 32 | * @parent bitballs/models/user.properties 33 | * 34 | * A unique identifier. 35 | **/ 36 | id: {type: 'number', identity: true}, 37 | /** 38 | * @property {String} bitballs/models/user.properties.email email 39 | * @parent bitballs/models/user.properties 40 | * 41 | * Email address representing the user 42 | **/ 43 | email: 'string', 44 | /** 45 | * @property {String} bitballs/models/user.properties.password password 46 | * @parent bitballs/models/user.properties 47 | * 48 | * Password for the user 49 | **/ 50 | password: 'string', 51 | 52 | /** 53 | * @property {String} bitballs/models/user.properties.newPassword newPassword 54 | * @parent bitballs/models/user.properties 55 | * 56 | * A placeholder for the user's new password. 57 | **/ 58 | newPassword: 'string', 59 | /** 60 | * @property {String} bitballs/models/user.properties.name name 61 | * @parent bitballs/models/user.properties 62 | * 63 | * User's full name as returned by the server 64 | **/ 65 | name: 'string', 66 | /** 67 | * @property {Boolean} bitballs/models/user.properties.isAdmin isAdmin 68 | * @parent bitballs/models/user.properties 69 | * 70 | * A boolean representing if the user has admin rights 71 | **/ 72 | isAdmin: 'boolean', 73 | /** 74 | * @property {Boolean} bitballs/models/user.properties.verified verified 75 | * @parent bitballs/models/user.properties 76 | * 77 | * A boolean representing if the user is verified 78 | **/ 79 | verified: 'boolean', 80 | /** 81 | * @property {String} bitballs/models/user.properties.verificationHash verificationHash 82 | * @parent bitballs/models/user.properties 83 | * 84 | * A unique hash representing user verification 85 | **/ 86 | verificationHash: 'string' 87 | }); 88 | 89 | /** 90 | * @constructor {can-list} bitballs/models/user.static.List List 91 | * @parent bitballs/models/user.static 92 | */ 93 | User.List = DefineList.extend('UserList', {"#": User}); 94 | 95 | User.queryLogic = new QueryLogic(User, bookshelfService); 96 | 97 | User.connection = superModel({ 98 | Map: User, 99 | List: User.List, 100 | url: { 101 | resource: "/services/users", 102 | contentType: "application/x-www-form-urlencoded" 103 | }, 104 | name: "user", 105 | queryLogic: User.queryLogic, 106 | updateInstanceWithAssignDeep: true 107 | }); 108 | 109 | 110 | export default User; 111 | -------------------------------------------------------------------------------- /public/models/youtube.js: -------------------------------------------------------------------------------- 1 | var platform = require("steal-platform" ); 2 | 3 | var promise; 4 | 5 | module.exports = function(){ 6 | if(promise) { 7 | return promise; 8 | } else { 9 | return promise = new Promise(function(resolve, reject){ 10 | if ( platform.isNode ) { 11 | reject({}); 12 | return; 13 | } 14 | window.onYouTubeIframeAPIReady = function(){ 15 | resolve(YT); 16 | }; 17 | var tag = document.createElement('script'); 18 | 19 | tag.src = "https://www.youtube.com/iframe_api"; 20 | document.head.appendChild(tag); 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /public/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitballs", 3 | "version": "0.4.1", 4 | "description": "", 5 | "homepage": "", 6 | "author": "Bitovi", 7 | "scripts": { 8 | "install": "node build.js", 9 | "test": "rm -rf ~/.mozilla && testee test.html --browsers firefox --reporter Spec" 10 | }, 11 | "main": "bitballs/index.stache!done-autorender", 12 | "files": [ 13 | "." 14 | ], 15 | "keywords": [], 16 | "license": "MIT", 17 | "dependencies": { 18 | "bootstrap": "^3.3.7", 19 | "can-assign": "^1.1.1", 20 | "can": "^5.0.0", 21 | "can-debug": "^2.0.1", 22 | "can-route-pushstate": "^5.0.7", 23 | "can-stache-route-helpers": "^1.1.1", 24 | "can-view-autorender": "^5.0.0", 25 | "can-zone": "^1.0.0", 26 | "done-autorender": "^2.6.3", 27 | "done-component": "^2.2.0", 28 | "done-css": "^3.0.2", 29 | "done-serve": "^3.0.0-pre.3", 30 | "done-ssr-middleware": "^3.0.0-pre.0", 31 | "funcunit": "^3.5.0", 32 | "generator-donejs": "3.0.0-pre.2", 33 | "jquery": "^3.1.1", 34 | "moment": "^2.10.6", 35 | "steal": "^2.1.5", 36 | "steal-less": "^1.3.4", 37 | "steal-platform": "0.0.4", 38 | "steal-qunit": "^1.0.1", 39 | "steal-stache": "^4.1.2", 40 | "steal-tools": "^2.0.6", 41 | "yeoman-environment": "^1.2.7" 42 | }, 43 | "devDependencies": { 44 | "can-route-hash": "^1.0.1", 45 | "donejs-cli": "^3.0.0-pre.3", 46 | "firebase-tools": "^3.9.1", 47 | "steal-conditional": "^1.1.1", 48 | "testee": "^0.8.0" 49 | }, 50 | "steal": { 51 | "configDependencies": [ 52 | "live-reload", 53 | "node_modules/can-zone/register", 54 | "node_modules/steal-conditional/conditional" 55 | ], 56 | "envs": { 57 | "server-production": { 58 | "renderingBaseURL": "https://bitballs-e69ca.firebaseapp.com/" 59 | } 60 | }, 61 | "meta": { 62 | "bootstrap/js/dropdown": { 63 | "deps": [ 64 | "jquery" 65 | ] 66 | } 67 | }, 68 | "bundle": [ 69 | "bitballs/components/game/details/", 70 | "bitballs/components/tournament/details/", 71 | "bitballs/components/tournament/list/", 72 | "bitballs/components/user/details/", 73 | "bitballs/components/user/list/", 74 | "bitballs/components/game/details/", 75 | "bitballs/components/player/list/", 76 | "bitballs/components/player/details/", 77 | "bitballs/components/404.component!" 78 | ], 79 | "plugins": [ 80 | "done-component", 81 | "done-css", 82 | "steal-less", 83 | "steal-stache" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /public/prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/service.js: -------------------------------------------------------------------------------- 1 | var ssr = require('done-ssr-middleware'); 2 | 3 | module.exports = ssr({ 4 | config: __dirname + "/package.json!npm", 5 | main: "bitballs/index.stache!done-autorender", 6 | liveReload: true 7 | }, { 8 | strategy: "incremental" 9 | }); 10 | -------------------------------------------------------------------------------- /public/test.html: -------------------------------------------------------------------------------- 1 | bitballs-client tests 2 | 3 | 6 |
7 | -------------------------------------------------------------------------------- /public/test.js: -------------------------------------------------------------------------------- 1 | import './models/test'; 2 | import './components/test'; 3 | import './util/test'; 4 | -------------------------------------------------------------------------------- /public/test/utils.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import F from 'funcunit'; 3 | 4 | export default { 5 | insertAndPopulateIframe: function (iframeParentSelector, frag) { 6 | var localStyles = $('head style').clone(); 7 | var iframe = $(document.createElement('iframe')); 8 | var iframeWindow; 9 | 10 | // Insert the iframe 11 | $(iframeParentSelector).append(iframe); 12 | 13 | // Get the context of the iframe (now that it's been added to the DOM) 14 | iframeWindow = iframe[0].contentWindow; 15 | 16 | // Populate the iframe 17 | iframe.contents().find('body').append(frag); 18 | iframe.contents().find('head').append(localStyles); 19 | 20 | // Convince FuncUnit that the iframe is loaded 21 | // (https://github.com/bitovi/funcunit/issues/139) 22 | F.documentLoaded = function () { return true; }; 23 | 24 | // Set the context of FuncUnit to be the iframe 25 | F.open(iframeWindow); 26 | } 27 | }; -------------------------------------------------------------------------------- /public/util/prefilter.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | export default $.ajaxPrefilter(function(options, originalOptions, jqXHR){ 4 | if(options.type === "POST" || options.type === "PUT"){ 5 | options.data = JSON.stringify(originalOptions.data); 6 | options.contentType = "application/json"; 7 | options.dataType = "json"; 8 | } 9 | }); -------------------------------------------------------------------------------- /public/util/test.html: -------------------------------------------------------------------------------- 1 | util 2 | 3 |
-------------------------------------------------------------------------------- /public/util/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donejs/bitballs/9670ae729d4cbfe0900d20e654d2bf96aa402c6a/public/util/test.js -------------------------------------------------------------------------------- /services/adminOnly.js: -------------------------------------------------------------------------------- 1 | module.exports = function ( respObj, status ) { 2 | if ( typeof respObj === "string" ) { 3 | respObj = { 4 | message: respObj 5 | }; 6 | } 7 | return function ( req, res, next ) { 8 | if ( req.isAdmin ) { 9 | next(); 10 | } else { 11 | if ( respObj ) { 12 | res.status( status || 401 ).json( respObj ); 13 | } else { 14 | res.status( status || 404 ).end(); 15 | } 16 | } 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /services/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var app = express(); 4 | 5 | app.use(bodyParser.urlencoded({ extended: false })); 6 | 7 | // parse application/json 8 | app.use(bodyParser.json()); 9 | 10 | module.exports = app; 11 | -------------------------------------------------------------------------------- /services/email.js: -------------------------------------------------------------------------------- 1 | var nodemailer = require('nodemailer'); 2 | //var checkit = require('checkit'); 3 | 4 | var transportOpts; 5 | 6 | if ( process.argv.indexOf( "--develop" ) !== -1 ) { 7 | var MailDev = require('maildev'); 8 | var maildev = new MailDev({ smtp: 1025 }); 9 | maildev.listen(); 10 | maildev.on( "new", function ( email ) { 11 | console.log( "email captured: http://localhost:1080" ); 12 | }); 13 | 14 | transportOpts = { 15 | port: 1025, 16 | ignoreTLS: true 17 | }; 18 | } else { 19 | transportOpts = process.env.EMAIL_CONFIG; 20 | } 21 | 22 | module.exports = function ( to, from, subject, body, cb ) { 23 | 24 | var transporter = nodemailer.createTransport( transportOpts ); 25 | 26 | return transporter.sendMail({ 27 | from: from, 28 | bcc: Array.isArray( to ) ? to.join( "," ) : to, 29 | subject: subject, 30 | html: body 31 | }, cb ); 32 | }; 33 | -------------------------------------------------------------------------------- /services/games.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {function} services/games /services/games 3 | * @parent bitballs.services 4 | * 5 | * @signature `GET /services/games` 6 | * Gets games from the database. 7 | * 8 | * GET /services/games? 9 | * where[tournamentId]=5& 10 | * withRelated[]=homeTeam&withRelated[]=awayTeam& 11 | * sortBy=round 12 | * 13 | * @param {Object} [where] Clause used to filter which games are returned. 14 | * @param {Array} [withRelated] Clause used to add related data. 15 | * @param {String} [sortBy] Clause used to sort the returned games. 16 | * @return {connectData} An object that contains the games: 17 | * 18 | * {data: [{ 19 | * id: Int, 20 | * tournamentId: Int, 21 | * round: String, 22 | * court: String, 23 | * videoUrl: String, 24 | * homeTeamId: Int, 25 | * awayTeamId: Int 26 | * }, ...]} 27 | * 28 | * @signature `POST /services/games` 29 | * Creates a game in the database. Only admins are allowed to create games. 30 | * 31 | * POST /services/games 32 | * { 33 | * "tournamentId": 1, 34 | * "round": 'Final', 35 | * "court": 'Old court', 36 | * "videoUrl": '?v=2141232213', 37 | * "homeTeamId": 1 38 | * "awayTeamId": 1 39 | * } 40 | * 41 | * @param {JSON} JSONBody The raw JSON properties of a game object 42 | * @return {JSON} Returns JSON with all the properties of the newly created object, including its id. 43 | * 44 | * { 45 | * "id": 9, 46 | * "tournamentId": 1, 47 | * "round": 'Final', 48 | * "court": 'Old court', 49 | * "videoUrl": '?v=2141232213', 50 | * "homeTeamId": 1 51 | * "awayTeamId": 1 52 | * } 53 | * 54 | * 55 | * @signature `GET /services/games/:id` 56 | * Gets a game by id from the database 57 | * 58 | * GET /services/games/9? 59 | * withRelated[]=homeTeam 60 | * 61 | * @param {Array} [withRelated] Clause used to add related data. 62 | * @return {JSON} An object that contains the game data: 63 | * 64 | * { 65 | * id: Int, 66 | * tournamentId: Int, 67 | * round: String, 68 | * court: String, 69 | * videoUrl: String, 70 | * homeTeamId: Int, 71 | * awayTeamId: Int 72 | * } 73 | * 74 | * @signature `PUT /services/games/:id` 75 | * Updates a game in the database. Only admins are allowed to update games. 76 | * 77 | * PUT /services/games/9 78 | * { 79 | * "tournamentId": 1, 80 | * "round": 'Final', 81 | * "court": 'New court', 82 | * "videoUrl": '?v=2141232213', 83 | * "homeTeamId": 1 84 | * "awayTeamId": 1 85 | * } 86 | * 87 | * @param {JSON} JSONBody The updated properties of the game object 88 | * @return {JSON} Returns JSON with all the properties of the updated object, including its id. 89 | * 90 | * { 91 | * "id": 9, 92 | * "tournamentId": 1, 93 | * "round": 'Final', 94 | * "court": 'New court', 95 | * "videoUrl": '?v=2141232213', 96 | * "homeTeamId": 1 97 | * "awayTeamId": 1 98 | * } 99 | * 100 | * 101 | * @signature `DELETE /services/games` 102 | * Deletes a game in the database. Only admins are allowed to delete games. 103 | * 104 | * DELETE /services/games/9 105 | * 106 | * @return {JSON} Returns an empty JSON object. 107 | * 108 | * {} 109 | */ 110 | 111 | var app = require("./app"); 112 | var Game = require("../models/game"); 113 | var adminOnly = require( "./adminOnly" ); 114 | var separateQuery = require("./separate-query"); 115 | 116 | app.get('/services/games', function(req, res){ 117 | var q = separateQuery(req.query), 118 | query = q.query, 119 | fetch = q.fetch; 120 | Game.collection().query(query).fetch(fetch).then(function(games){ 121 | res.send({data: games.toJSON()}); 122 | }); 123 | }); 124 | 125 | app.get('/services/games/:id', function(req, res){ 126 | var q = separateQuery(req.query), 127 | query = q.query, 128 | fetch = q.fetch; 129 | new Game({id: req.params.id}).query(query).fetch(fetch).then(function(game){ 130 | if(game){ 131 | res.send(game.toJSON()); 132 | }else { 133 | res.status(404).send(); 134 | } 135 | }); 136 | }); 137 | 138 | app.put('/services/games/:id', adminOnly( "Must be an admin to update games" ), function(req, res){ 139 | new Game({id: req.params.id}).save(req.body).then(function(game){ 140 | res.send(game.toJSON()); 141 | }); 142 | }); 143 | 144 | app.delete('/services/games/:id', adminOnly( "Must be an admin to delete games" ), function(req, res){ 145 | new Game({id: req.params.id}).destroy().then(function(game){ 146 | res.send({}); 147 | }); 148 | }); 149 | 150 | app.post('/services/games', adminOnly( "Must be an admin to create games" ), function(req, res) { 151 | new Game(req.body).save().then(function(game){ 152 | res.send({id: game.get('id')}); 153 | }, function(e){ 154 | res.status(500).send(e); 155 | }); 156 | }); 157 | 158 | module.exports = Game; 159 | -------------------------------------------------------------------------------- /services/players.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {function} services/players /services/players 3 | * @parent bitballs.services 4 | * 5 | * @signature `GET /services/players` 6 | * Gets players from the database 7 | * 8 | * GET /services/games? 9 | * where[playerId]=5& 10 | * sortBy=startRank 11 | * 12 | * @param {Object} [where] Clause used to filter which players are returned. 13 | * @param {String} [sortBy] Clause used to sort the returned players 14 | * @return {JSON} An object that contains the player data: 15 | * 16 | * {data: [{ 17 | * id: Int, 18 | * name: String, // Player name 19 | * weight: Int, // Player weight, in lbs 20 | * height: Int, // Player height, in inches 21 | * birthday: Date, // Player birthday 22 | * profile: String, // Player description/bio 23 | * startRank: String // Starting Rank 24 | * }]} 25 | * 26 | * @signature `POST /services/players` 27 | * Adds a player to the database. Only admins are allowed to create players. 28 | * 29 | * POST /services/players 30 | * { 31 | * "name": "Harper Lee", 32 | * "weight": 190, 33 | * "height": 72, 34 | * "birthday": "1990-01-22", 35 | * "profile": "Author of 'To Kill a Mockingbird'", 36 | * "startRank": "novice" 37 | * } 38 | * 39 | * @param {JSON} JSONBody The Raw JSON properties of a player object 40 | * @return {JSON} Returns JSON with all the properties of the newly created object, including its id 41 | * 42 | * { 43 | * "id": 9, 44 | * "name": "Harper Lee", 45 | * "weight": 190, 46 | * "height": 72, 47 | * "birthday": "1990-01-22", 48 | * "profile": "Author of 'To Kill a Mockingbird'", 49 | * "startRank": "novice" 50 | * } 51 | * 52 | * @signature `GET /services/players/:id` 53 | * Gets a player by id from the database. 54 | * 55 | * GET /services/players/9 56 | * 57 | * @return {JSON} An object that contains the player data: 58 | * 59 | * {data: [{ 60 | * id: Int, 61 | * name: String, // Player name 62 | * weight: Int, // Player weight, in lbs 63 | * height: Int, // Player height, in inches 64 | * birthday: Date // Player birthday 65 | * profile: String // Player description/bio 66 | * startRank: String // Starting Rank 67 | * }]} 68 | * 69 | * @signature `PUT /services/players/:id` 70 | * Updates a player in the database. Only admins are allowed to update players. 71 | * 72 | * PUT /services/players/9 73 | * { 74 | * "name": "Harper Lee", 75 | * "weight": 190, 76 | * "height": 72, 77 | * "birthday": "1990-01-22", 78 | * "profile": "Author of 'To Kill a Mockingbird' and `Absalom, Absalom`", 79 | * "startRank": "novice" 80 | * } 81 | * 82 | * @param {JSON} JSONBody The updated properties of the player object 83 | * @return {JSON} Returns JSON with all the properties of the updated object, including its id. 84 | * 85 | * { 86 | * "name": "Harper Lee", 87 | * "weight": 190, 88 | * "height": 72, 89 | * "birthday": "1990-01-22", 90 | * "profile": "Author of 'To Kill a Mockingbird' and `Absalom, Absalom`", 91 | * "startRank": "novice" 92 | * } 93 | * 94 | * @signature `DELETE /services/players/:id` 95 | * Deletes a player in the database. Only admins are allowed to delete players. 96 | * 97 | * DELETE /services/players/9 98 | * 99 | * @return {JSON} Returns and empty JSON object. 100 | * 101 | * {} 102 | */ 103 | 104 | var app = require("./app"); 105 | var Player = require("../models/player"); 106 | var adminOnly = require( "./adminOnly" ); 107 | var separateQuery = require("./separate-query"); 108 | 109 | app.get('/services/players', function(req, res){ 110 | var q = separateQuery(req.query), 111 | query = q.query, 112 | fetch = q.fetch; 113 | Player.collection().query(query).fetch(fetch).then(function(players){ 114 | res.send({data: players.toJSON()}); 115 | }); 116 | }); 117 | 118 | app.get('/services/players/:id', function(req, res){ 119 | var q = separateQuery(req.query), 120 | query = q.query, 121 | fetch = q.fetch; 122 | new Player({id: req.params.id}).query(query).fetch(fetch).then(function(player){ 123 | res.send(player.toJSON()); 124 | }); 125 | }); 126 | 127 | app.put('/services/players/:id', adminOnly( "Must be an admin to update players" ), function(req, res){ 128 | var cleaned = clean(req.body); 129 | new Player({id: req.params.id}).save(cleaned).then(function(player){ 130 | res.send(player.toJSON()); 131 | }); 132 | }); 133 | 134 | app.delete('/services/players/:id', adminOnly( "Must be an admin to delete players" ), function(req, res){ 135 | new Player({id: req.params.id}).destroy().then(function(player){ 136 | res.send({_destroyed: true}); 137 | }); 138 | }); 139 | 140 | app.post('/services/players', adminOnly( "Must be an admin to create players" ), function(req, res) { 141 | new Player(clean(req.body)).save().then(function(player){ 142 | res.send({id: player.get('id')}); 143 | }, function(e){ 144 | res.status(500).send(e); 145 | }); 146 | }); 147 | 148 | module.exports = Player; 149 | 150 | var clean = function(data){ 151 | if(data.name === ''){ 152 | delete data.name; 153 | } 154 | 155 | if(data.weight) { 156 | data.weight = parseInt(data.weight, 10); 157 | } 158 | 159 | if(data.height) { 160 | data.height = parseInt(data.height, 10); 161 | } 162 | 163 | return data; 164 | }; 165 | -------------------------------------------------------------------------------- /services/separate-query.js: -------------------------------------------------------------------------------- 1 | var fetchKeys = [ 'require', 'columns', 'transacting', 'withRelated' ]; 2 | 3 | module.exports = function(input) { 4 | var output = { 5 | query: {}, 6 | fetch: {}, 7 | }; 8 | 9 | for (var key in input) { 10 | if (fetchKeys.indexOf(key) >= 0) { 11 | output.fetch[key] = input[key]; 12 | } 13 | else { 14 | output.query[key] = input[key]; 15 | } 16 | } 17 | 18 | return output; 19 | }; 20 | -------------------------------------------------------------------------------- /services/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {function} services/session /services/session 3 | * @parent bitballs.services 4 | * 5 | * @signature `GET /services/session` 6 | * Gets the current session, if any. 7 | * 8 | * GET /services/session 9 | * 10 | * @return {JSON} An object containing the user object with sensitive properties omitted. 11 | * 12 | * { 13 | * user: { 14 | * "id": Int, 15 | * "name": String, // Optional name 16 | * "email": String, // User email address 17 | * "isAdmin": Boolean, // Whether user is an admin 18 | * "verified": Boolean // Whether user has verified an email address 19 | * } 20 | * } 21 | * 22 | * @signature `POST /services/session` 23 | * If a user object is provided with a valid password/email combination, logs in the current user and creates a session. 24 | * 25 | * POST /services/session 26 | * { 27 | * user: { 28 | * { 29 | * "email": "addyfizzle@publicdefenders.org" 30 | * "password": "H3HLJ2HIO4" 31 | * } 32 | * } 33 | * } 34 | * 35 | * @return {JSON} An object containing the logged in user object with sensitive properties omitted. 36 | * 37 | * { 38 | * user: { 39 | * "id": 9, 40 | * "name": "Atticus Finch", 41 | * "email": "addyfizzle@publicdefenders.org", 42 | * "isAdmin": false, 43 | * "verified": true 44 | * } 45 | * } 46 | * 47 | * @signature `DELETE /services/session` 48 | * Logs the current user out. 49 | * 50 | * DELETE /services/session 51 | * 52 | * @return {JSON} Returns an empty JSON object. 53 | * 54 | * {} 55 | */ 56 | 57 | var app = require("./app"); 58 | var User = require("../models/user"); 59 | var separateQuery = require("./separate-query"); 60 | var _ = require("lodash"); 61 | var passport = require('passport'); 62 | var bCrypt = require("bcrypt-nodejs"); 63 | 64 | passport.serializeUser(function(user, done) { 65 | done(null, user.id); 66 | }); 67 | 68 | passport.deserializeUser(function(id, done) { 69 | new User({ 70 | 'id' : id 71 | }).fetch().then(function(user) { 72 | done(null, user); 73 | }, function(err) { 74 | done(err); 75 | }); 76 | }); 77 | 78 | var expressSession = require('express-session'); 79 | 80 | var cookieSecret = process.env.COOKIE_SECRET || 'devSecret'; 81 | 82 | app.use(expressSession({ 83 | secret : cookieSecret, 84 | resave : false, 85 | saveUninitialized : false 86 | })); 87 | app.use(passport.initialize()); 88 | app.use(passport.session()); 89 | 90 | app.use(function ( req, res, next ) { 91 | req.isAdmin = ( req.user && req.user.attributes && req.user.attributes.isAdmin ) ? true : false; 92 | next(); 93 | }); 94 | 95 | var isValidPassword = function(user, password) { 96 | return bCrypt.compareSync(password, user.get("password") ); 97 | }; 98 | 99 | app.get('/services/session', function(req, res) { 100 | if (req.user) { 101 | res.send({id: req.user.id, user: _.omit(req.user.toJSON(), "password")}); 102 | } else { 103 | res.status(404).send(JSON.stringify({ 104 | message : "No session" 105 | })); 106 | } 107 | }); 108 | 109 | app.post('/services/session', function(req, res, next) { 110 | var email = req.body.user.email, 111 | password = req.body.user.password; 112 | 113 | var q = separateQuery(req.query), 114 | query = q.query, 115 | fetch = q.fetch; 116 | 117 | new User({ 118 | 'email': email 119 | }).query(query).fetch(fetch).then(function(user) { 120 | if(user && isValidPassword(user, password)) { 121 | req.logIn(user, function(err) { 122 | if (err) { 123 | next(err); 124 | } 125 | return res.json({user: _.omit(req.user.toJSON(), "password")}); 126 | }); 127 | } else { 128 | // Security conventions dictate that you shouldn't tell the user whether 129 | // it was their username or their password that was the problem 130 | return res.status(401).json({message: "Incorrect username or password"}); 131 | } 132 | 133 | }, function(error) { 134 | 135 | console.log('User error ' + email, error); 136 | return res.status( 500 ).json( error ); 137 | 138 | }); 139 | }); 140 | 141 | app['delete']("/services/session", function(req, res){ 142 | req.logout(); 143 | res.json({}); 144 | }); 145 | -------------------------------------------------------------------------------- /services/stats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {function} services/stats /services/stats 3 | * @parent bitballs.services 4 | * 5 | * @signature `GET /services/stats` 6 | * Gets stats from the database. 7 | * 8 | * GET /services/stats? 9 | * where[gameId]=5& 10 | * withRelated[]=player 11 | * sortBy=time 12 | * 13 | * @param {Object} [where] Clause used to filter which stats are returned. 14 | * @param {Array} [withRelated] Clause used to add related data. 15 | * @param {String} [sortBy] Clause used to sort the returned stats. 16 | * @return {JSON} An object that contains the stats: 17 | * 18 | * {data: [{ 19 | * id: Int, 20 | * gameId: Int, // Related game 21 | * playerId: Int, // Related player 22 | * type: String, // "1P", "1PA", etc 23 | * time: Int, // time of stat in seconds 24 | * default: Int, // Not currently used, but available. 25 | * }, ...]} 26 | * 27 | * @signature `POST /services/stats` 28 | * Creates a stat in the database. Only admins are allowed to create stats. 29 | * 30 | * POST /services/stats 31 | * { 32 | * "gameId": 6, 33 | * "playerId": 15, 34 | * "type": "1P", 35 | * "time": 60 36 | * } 37 | * @param {JSON} JSONBody The raw JSON properties of a stat object. 38 | * @return {JSON} Returns JSON with all the properties of the newly created object, including its id. 39 | * 40 | * { 41 | * "id": 9 42 | * "gameId": 6, 43 | * "playerId": 15, 44 | * "type": "1P", 45 | * "time": 60 46 | * } 47 | * 48 | * @signature `GET /services/stats/:id` 49 | * Gets a stat by id from the database. 50 | * 51 | * GET /services/stats/5? 52 | * withRelated[]=player 53 | * 54 | * @param {Array} [withRelated] Clause used to add related data. 55 | * @return {JSON} An object that contains the stats: 56 | * 57 | * { 58 | * id: Int, 59 | * gameId: Int, // Related game 60 | * playerId: Int, // Related player 61 | * type: String, // "1P", "1PA", etc 62 | * time: Int, // time of stat in seconds 63 | * default: Int, // Not currently used, but available. 64 | * } 65 | * 66 | * @signature `PUT /services/stats/:id` 67 | * Updates a stat in the database. Only admins are allowed to update stats. 68 | * 69 | * PUT /services/stats/9 70 | * { 71 | * "gameId": 6, 72 | * "playerId": 15, 73 | * "type": "1P", 74 | * "time": 120 75 | * } 76 | * 77 | * @param {JSON} JSONBody The updated properties of the stat object. 78 | * @return {JSON} Returns JSON with all the properties of the updated object, including its id. 79 | * 80 | * { 81 | * "id": 9 82 | * "gameId": 6, 83 | * "playerId": 15, 84 | * "type": "1P", 85 | * "time": 60 86 | * } 87 | * 88 | * @signature `DELETE /services/stats/:id` 89 | * Deletes a stat in the database. Only admins are allowed to delete stats. 90 | * 91 | * DELETE /services/stats/9 92 | * 93 | * @return {JSON} Returns an empty JSON object. 94 | * 95 | * {} 96 | */ 97 | 98 | var app = require("./app"); 99 | var Stat = require("../models/stat"); 100 | var adminOnly = require( "./adminOnly" ); 101 | var separateQuery = require("./separate-query"); 102 | 103 | app.get('/services/stats', function(req, res){ 104 | var q = separateQuery(req.query), 105 | query = q.query, 106 | fetch = q.fetch; 107 | Stat.collection().query(query).fetch(fetch).then(function(stats){ 108 | res.send({data: stats.toJSON()}); 109 | }); 110 | }); 111 | 112 | app.get('/services/stats/:id', function(req, res){ 113 | var q = separateQuery(req.query), 114 | query = q.query, 115 | fetch = q.fetch; 116 | new Stat({id: req.params.id}).query(query).fetch(fetch).then(function(stat){ 117 | res.send(stat.toJSON()); 118 | }); 119 | }); 120 | 121 | app.put('/services/stats/:id', adminOnly( "Must be an admin to update stats" ), function(req, res){ 122 | new Stat({id: req.params.id}).save(req.body).then(function(stat){ 123 | res.send(stat.toJSON()); 124 | }); 125 | }); 126 | 127 | app.delete('/services/stats/:id', adminOnly( "Must be an admin to delete stats" ), function(req, res){ 128 | new Stat({id: req.params.id}).destroy().then(function(stat){ 129 | res.send({}); 130 | }); 131 | }); 132 | 133 | app.post('/services/stats', adminOnly( "Must be an admin to create stats" ), function(req, res) { 134 | new Stat(clean(req.body)).save().then(function(stat){ 135 | res.send({id: stat.get('id')}); 136 | }, function(e){ 137 | res.status(500).send(e); 138 | }); 139 | }); 140 | 141 | module.exports = Stat; 142 | 143 | var clean = function(data){ 144 | if(data.time) { 145 | data.time = parseInt(data.time, 10); 146 | } 147 | 148 | return data; 149 | }; 150 | -------------------------------------------------------------------------------- /services/tournaments.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module {function} services/tournaments /services/tournaments 3 | * @parent bitballs.services 4 | * 5 | * @signature `GET /services/tournaments` 6 | * Gets tournaments from the database. 7 | * 8 | * GET /services/tournaments? 9 | * where[date]=2016-01-01& 10 | * sortBy=date 11 | * 12 | * @param {Object} [where] Clause used to filter which tournaments are returned 13 | * @param {String} [sortBy] Clause used to sort the returned stats 14 | * @return {JSON} An object that contains the stats: 15 | * 16 | * {data: [{ 17 | * id: Int, 18 | * date: Date 19 | * }, ...]} 20 | * 21 | * @signature `POST /services/tournaments` 22 | * Creates a tournament in the database. Only admins are allowed to create tournaments. 23 | * 24 | * POST /services/tournaments 25 | * { 26 | * "date": "2016-01-01" 27 | * } 28 | * 29 | * @param {JSON} JSONBody The raw JSON properties of a tournament object 30 | * @return {JSON} Returns JSON with all the properties of the newly created object, including its id. 31 | * 32 | * { 33 | * "id": 9, 34 | * "date": "2016-01-01" 35 | * } 36 | * 37 | * @signature `GET /services/tournaments/:id` 38 | * Gets a tournament by id from the database. 39 | * 40 | * GET /services/tournaments/9 41 | * 42 | * @return {JSON} An object that contains the tournament data: 43 | * 44 | * { 45 | * id: Int, 46 | * date: Date 47 | * } 48 | * 49 | * @signature `PUT /services/tournaments/:id` 50 | * Updates a tournament in the database. Only admins are allowed to update tournaments. 51 | * 52 | * PUT /services/tournaments/9 53 | * { 54 | * "date": "2015-01-01" 55 | * } 56 | * 57 | * @param {JSON} JSONBody The updated properties of the tournament object. 58 | * @return {JSON} Returns JSON with all the properties of the updated object, including its id. 59 | * 60 | * { 61 | * "id": 9, 62 | * "date": "2015-01-01" 63 | * } 64 | * 65 | * @signature `DELETE /services/tournaments/:id` 66 | * Deletes a tournament in the database. Only admins are allowed to delete stats. 67 | * 68 | * DELETE /services/tournaments/9 69 | * 70 | * @return {JSON} Returns an empty JSON object. 71 | * 72 | * {} 73 | */ 74 | 75 | var app = require("./app"); 76 | var Tournament = require("../models/tournament"); 77 | var adminOnly = require( "./adminOnly" ); 78 | var separateQuery = require("./separate-query"); 79 | 80 | app.get('/services/tournaments', function(req, res){ 81 | var q = separateQuery(req.query), 82 | query = q.query, 83 | fetch = q.fetch; 84 | Tournament.collection().query(query).fetch(fetch).then(function(tournaments){ 85 | res.send({data: tournaments.toJSON()}); 86 | }); 87 | }); 88 | 89 | app.get('/services/tournaments/:id', function(req, res){ 90 | var q = separateQuery(req.query), 91 | query = q.query, 92 | fetch = q.fetch; 93 | new Tournament({id: req.params.id}).query(query).fetch(fetch).then(function(tournament){ 94 | if(tournament){ 95 | res.send(tournament.toJSON()); 96 | } else { 97 | res.status(404).send(); 98 | } 99 | }); 100 | }); 101 | 102 | app.put('/services/tournaments/:id', adminOnly( "Must be an admin to update tournaments" ), function(req, res){ 103 | new Tournament({id: req.params.id}).save(req.body).then(function(tournament){ 104 | res.send(tournament.toJSON()); 105 | }); 106 | }); 107 | 108 | app.delete('/services/tournaments/:id', adminOnly( "Must be an admin to delete tournaments" ), function(req, res){ 109 | new Tournament({id: req.params.id}).destroy().then(function(tournament){ 110 | res.send({}); 111 | }); 112 | }); 113 | 114 | app.post('/services/tournaments', adminOnly( "Must be an admin to create tournaments" ), function(req, res) { 115 | new Tournament(req.body).save().then(function(tournament){ 116 | res.send({id: tournament.get('id')}); 117 | }, function(e){ 118 | res.status(500).send(e); 119 | }); 120 | }); 121 | 122 | module.exports = Tournament; 123 | -------------------------------------------------------------------------------- /test-script.md: -------------------------------------------------------------------------------- 1 | # Test Script 2 | 3 | ## Setup 4 | 5 | To clear the database, run: 6 | 7 | ``` 8 | ./node_modules/.bin/db-migrate reset 9 | ``` 10 | 11 | until you can't run it anymore 12 | 13 | Then run: 14 | 15 | ``` 16 | ./node_modules/.bin/db-migrate up 17 | ``` 18 | 19 | To run your app so you can see what's going on, run: 20 | 21 | ``` 22 | node index.js --develop --slow 23 | ``` 24 | 25 | This puts a 1s delay on all responses. 26 | 27 | ## Verify all pages work when there is no data and no one is logged in. 28 | 29 | - Go to every page, make sure things look right. 30 | 31 | ## Create user 32 | 33 | - try to create a user without an `email` or password. Clicking `Register` multiple times should not spawn multiple requests. 34 | 35 | 36 | - try to create a user without a `password` 37 | 38 | - create a user. You should be logged in and the user's email locked. 39 | 40 | ## Create Players 41 | 42 | - Make sure the placeholder text is showing up. 43 | - Create a player with all the right info. 44 | - Create a player w/o a name ... you shouldn't be able to. 45 | 46 | 47 | ## Update a player 48 | 49 | - you should be able to cancel 50 | - you should be able to update 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | - logout and try to log back in 61 | 62 | ## Update user's password 63 | -------------------------------------------------------------------------------- /theme/static/content_list.js: -------------------------------------------------------------------------------- 1 | steal("can/control","jquery","can/observe",function(Control, $){ 2 | 3 | var contentList = function(sections, tag){ 4 | var element = $("<"+tag+">"); 5 | $.each(sections, function(i, section){ 6 | $li = $("
  • "); 7 | $a = $("").attr("href","#"+section.id).text(section.text); 8 | element.append( $li.append($a) ); 9 | 10 | if(section.sections && section.sections.length) { 11 | $li.append( contentList(section.sections, tag) ); 12 | } 13 | }); 14 | return element; 15 | }; 16 | 17 | return can.Control.extend({ 18 | init: function() { 19 | 20 | var sections = []; 21 | 22 | this.collectSignatures().each(function(ix) { 23 | var h2 = $('h2', this); 24 | this.id = 'sig_' + h2.text().replace(/\s/g,"").replace(/[^\w]/g,"_"); 25 | //this.id = encodeURIComponent(h2.text()); 26 | sections.push({id: this.id, text: h2.text()}); 27 | }); 28 | 29 | var headingStack = [], 30 | last = function(){ 31 | return headingStack[ headingStack.length -1 ] 32 | }; 33 | 34 | var ch = this.collectHeadings().each(function(ix) { 35 | var el = $(this); 36 | //change formatting here from default to match this: 37 | // ## This is My Heading -->

    38 | this.id = el.text().toLowerCase().replace(/[^\w]/g,"-").replace(/\s/g,""); 39 | var num = +this.nodeName.substr(1); 40 | var section = { 41 | id: this.id, 42 | text: el.text(), 43 | num: num, 44 | sections: [] 45 | }; 46 | 47 | while(last() && (last().num >= num) ) { 48 | headingStack.pop(); 49 | } 50 | 51 | if(!headingStack.length) { 52 | sections.push(section); 53 | headingStack.push(section); 54 | } else { 55 | last().sections.push(section); 56 | headingStack.push(section); 57 | } 58 | }); 59 | 60 | // make sure to hide TOC, but still do id processing 61 | if(window.docObject.hideContents){ 62 | return; 63 | } 64 | 65 | this.element.html( contentList(sections, 66 | ( ( window.docObject.outline && window.docObject.outline.tag ) || "ul" ).toLowerCase() ) ); 67 | 68 | if(window.location.hash.length) { 69 | var id = window.location.hash.replace('#', ''), 70 | anchor = document.getElementById(id); 71 | 72 | if(anchor) { 73 | anchor.scrollIntoView(true); 74 | } 75 | } 76 | }, 77 | collectSignatures: function() { 78 | var cloned = $('.content .signature').clone(); 79 | // remove release numbers 80 | cloned.find(".release").remove(); 81 | return cloned; 82 | }, 83 | collectHeadings: function() { 84 | //change the depth so we capture all our headings 85 | var depth = ( window.docObject.outline && window.docObject.outline.depth ) || 3; 86 | var headings = []; 87 | for(var i = 0; i < depth; i++) { 88 | headings.push("h"+(i+2)); 89 | } 90 | return $('.content .comment').find(headings.join(",")); 91 | } 92 | }); 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /theme/static/static.js: -------------------------------------------------------------------------------- 1 | steal( 2 | // documentjs's dependencies 3 | "./content_list.js", 4 | "./frame_helper.js", 5 | "./versions.js", 6 | "./styles/styles.less!", 7 | "prettify.js", 8 | 9 | function(ContentList, FrameHelper, Versions){ 10 | var codes = document.getElementsByTagName("code"); 11 | for(var i = 0; i < codes.length; i ++){ 12 | var code = codes[i]; 13 | if(code.parentNode.nodeName.toUpperCase() === "PRE"){ 14 | code.className = code.className +" prettyprint" 15 | } 16 | } 17 | prettyPrint(); 18 | 19 | new ContentList(".contents"); 20 | new FrameHelper(".docs"); 21 | new Versions( $("#versions, .sidebar-title:first") ); 22 | }); -------------------------------------------------------------------------------- /theme/templates/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = function(docMap, config, getCurrent){ 2 | return { 3 | ifValidSource: function (src, options) { 4 | if (src && config.source){ 5 | return options.fn(this); 6 | }else{ 7 | return options.inverse(this); 8 | } 9 | }, 10 | urlSource: function(src, options){ 11 | var source = getCurrent().source; 12 | 13 | return source + "/tree/master/" + src; 14 | } 15 | }; 16 | }; 17 | --------------------------------------------------------------------------------