Skip to content

Commit c5e671c

Browse files
committedAug 14, 2015
Initial commit
0 parents  commit c5e671c

27 files changed

+1204
-0
lines changed
 

‎.bowerrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"directory": "public/components",
3+
"json": "bower.json"
4+
}

‎.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
node_modules/
2+
public/components
3+
.sass-cache
4+
semantic/
5+
public/semantic.js
6+
public/semantic.css
7+
data/

‎app.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
3+
var express = require('express'),
4+
config = require('./config/config'),
5+
db = require('./app/models');
6+
7+
var app = express();
8+
9+
require('./config/express')(app, config);
10+
11+
db.sequelize
12+
.sync()
13+
.then(function () {
14+
app.listen(config.port);
15+
}).catch(function (e) {
16+
throw new Error(e);
17+
});
18+

‎app/controllers/game.js

+265
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
var express = require('express'),
2+
router = express.Router(),
3+
router2 = express.Router(),
4+
Sequelize = require('sequelize'),
5+
db = require('../models');
6+
7+
/**
8+
* Get a static list of unique Players given a list of Games.
9+
*/
10+
function getStaticPlayerList(games)
11+
{
12+
var list = [];
13+
games.forEach(function(game) {
14+
if (!game.Players) return;
15+
game.Players.forEach(function(player) {
16+
var exists = false;
17+
list.forEach(function(li) {
18+
exists = exists || (li.id == player.id);
19+
});
20+
if (!exists)
21+
list.push({id: player.id, name: player.name, email: player.email});
22+
});
23+
});
24+
25+
return list;
26+
}
27+
28+
module.exports = function (app) {
29+
app.use('/games', router);
30+
app.use('/game', router2);
31+
};
32+
33+
/**
34+
* Page templates:
35+
*/
36+
router2.get('/:id/', function(req, res, next) {
37+
db.Game.find({ where: { id: req.params.id }}).then(function (game) {
38+
// TODO: redirect to index but put the messages in a session (so URL is updated)
39+
if (!game) res.render('index', {messages: ["Cannot find that Game!"] });
40+
else res.render('game', {gameStart: game.when, gameThreshold: game.threshold, gameId: game.id});
41+
});
42+
});
43+
44+
/**
45+
* JSON routes:
46+
*/
47+
// See the list of the game history (most recent first)
48+
// TODO: pagination eventually?
49+
router.get('/all', function (req, res, next) {
50+
db.Game.findAll({ order: [['when', 'DESC']]}).then(function (games) {
51+
// Just return list of id / timestamps
52+
var ret = {games: []};
53+
var details = {};
54+
games.forEach(function(game) {
55+
details = {id: game.id, when: game.when};
56+
ret.games.push(details);
57+
});
58+
res.json(ret);
59+
});
60+
});
61+
62+
// See the list of the games for the last week (for the homepage)
63+
router.get('/last-week', function (req, res, next) {
64+
// Calculate midnight of the current day
65+
var midnight = new Date();
66+
midnight.setHours(0, 0, 0, 0);
67+
midnight.setDate(midnight.getDate() - 7);
68+
var ret = {games: []};
69+
db.Game.findAll({
70+
where: {when: {$gte: midnight}},
71+
include: [db.Player, db.Goal],
72+
order: [['when', 'DESC']]
73+
}).then(function (games) {
74+
75+
// For the week, actually return player goals
76+
// NOTE: To aid in the D3 viz, we ensure that all games have the
77+
// same list of players, in the same order. So start by building
78+
// the information for the players list for this week.
79+
var playerList = getStaticPlayerList(games);
80+
console.log(playerList);
81+
82+
// Now build the information for the games themselves:
83+
var details = {};
84+
games.forEach(function(game) {
85+
details = {id: game.id, when: game.when};
86+
details.players = [];
87+
// Instantiate the player list in the same order with the same values
88+
playerList.forEach(function(player) {
89+
details.players.push({id: player.id, name: player.name, email: player.email, goals: 0});
90+
});
91+
game.Players.forEach(function(player){
92+
// Count this user's goals
93+
var goalCount = 0;
94+
game.Goals.forEach(function(goal) {
95+
if (goal.PlayerId == player.id) goalCount++;
96+
});
97+
98+
// Update the goals for this player (others remain at 0)
99+
details.players.forEach(function(inner) {
100+
if (player.id == inner.id) inner.goals = goalCount;
101+
});
102+
});
103+
ret.games.push(details);
104+
});
105+
106+
res.json(ret);
107+
});
108+
});
109+
110+
// Details for a single game
111+
router.get('/:id', function (req, res, next) {
112+
// Return value
113+
var targetGame = {};
114+
115+
db.Game.find({ where: { id: req.params.id }}).then(function (game) {
116+
if (!game) res.json(targetGame);
117+
else
118+
{
119+
targetGame.when = game.when;
120+
targetGame.threshold = game.threshold;
121+
targetGame.players = {};
122+
123+
// Get Players Scores
124+
game.getPlayers().then(function(players) {
125+
// Initialize goals to zero
126+
players.forEach(function (player) {
127+
targetGame.players[player.id] = {
128+
id: player.id,
129+
name: player.name,
130+
email: player.email,
131+
goals: 0
132+
};
133+
});
134+
135+
// Now record actual counts
136+
game.getGoals().then(function(goals) {
137+
goals.forEach(function(goal) {
138+
targetGame.players[goal.PlayerId].goals++;
139+
});
140+
141+
// Send back data
142+
res.json(targetGame);
143+
});
144+
});
145+
}
146+
});
147+
});
148+
149+
// Create
150+
router.post('/create', function(req, res, next) {
151+
db.Game.create({when: new Date()}).then(function(game) {
152+
res.json({success: true, id: game.id});
153+
});
154+
});
155+
156+
/**
157+
* Helper methods.
158+
*/
159+
// Add a Player to an existing game
160+
router.post('/:id/add/player/:pid', function(req, res, next) {
161+
console.log('Adding a Player to a Game');
162+
db.Game.find({ where: { id: req.params.id }}).then(function (game) {
163+
if (!game) res.json({error: 'Invalid Game ID'});
164+
else
165+
{
166+
db.Player.find({ where: { id: req.params.pid }}).then(function(player) {
167+
if (!player) res.json({error: 'Invalid Player ID'});
168+
else
169+
{
170+
game.addPlayer(player).then(function() {
171+
res.json({success: 'Player ' + player.name + ' added to Game ID ' + game.id});
172+
});
173+
}
174+
});
175+
}
176+
});
177+
});
178+
179+
// Remove a player from a game (they must not have any goals)
180+
router.post('/:id/remove/player/:pid', function(req, res, next) {
181+
console.log('Removing a Player from a Game');
182+
db.Game.find({ where: { id: req.params.id }}).then(function (game) {
183+
if (!game) res.json({error: 'Invalid Game ID'});
184+
else
185+
{
186+
db.Player.find({ where: { id: req.params.pid }}).then(function(player) {
187+
if (!player) res.json({error: 'Invalid Player ID'});
188+
else
189+
{
190+
game.getGoals().then(function(goals) {
191+
var count = 0;
192+
goals.forEach(function(goal) {
193+
if (goal.PlayerId == player.id) count++;
194+
});
195+
196+
if (count > 0)
197+
{
198+
res.json({error: "Cannot remove a Player that has already scored. FINISH THAT GAME!"});
199+
}
200+
else
201+
{
202+
game.removePlayer(player).then(function() {
203+
res.json({success: 'Player ' + player.name + ' removed from Game ID ' + game.id});
204+
});
205+
}
206+
});
207+
}
208+
});
209+
}
210+
});
211+
});
212+
213+
// Score a goal
214+
router.post('/:id/goal/player/:pid', function(req, res, next) {
215+
console.log('GOOOOOOOOOOAL!');
216+
db.Game.find({ where: { id: req.params.id }}).then(function (game) {
217+
if (!game) res.json({error: 'Invalid Game ID'});
218+
else
219+
{
220+
db.Player.find({ where: { id: req.params.pid }}).then(function(player) {
221+
if (!player) res.json({error: 'Invalid Player ID'});
222+
else
223+
{
224+
// Verify that the game is still open
225+
var sql = "SELECT PlayerId, COUNT(PlayerId) AS c FROM Goals WHERE GameId=? GROUP BY PlayerId ORDER BY c DESC";
226+
db.sequelize.query(sql, { replacements: [game.id], type: Sequelize.QueryTypes.SELECT }).then(function(rows) {
227+
if (rows.length > 0 && rows[0].c >= game.threshold)
228+
{
229+
res.json({error: 'Game is already closed, Winner (PlayerId): ' + rows[0].PlayerId});
230+
}
231+
else
232+
{
233+
// Create the goal
234+
db.Goal.create({ when: new Date() }).then(function(goal) {
235+
goal.setPlayer(player);
236+
goal.setGame(game);
237+
res.json({success: 'Player ' + player.name + ': GOOOOOOOOOOOOOAL!', id: goal.id});
238+
});
239+
}
240+
});
241+
}
242+
});
243+
}
244+
});
245+
});
246+
247+
// Undo a goal (delete)
248+
router.delete('/:id/goal/:gid', function(req, res, next) {
249+
console.log('Undoing accidental goal button press...');
250+
db.Game.find({ where: { id: req.params.id }}).then(function (game) {
251+
if (!game) res.json({error: 'Invalid Game ID'});
252+
else
253+
{
254+
db.Goal.find({ where: { id: req.params.gid }}).then(function(goal) {
255+
if (!goal) res.json({error: 'Invalid Goal ID'});
256+
else
257+
{
258+
goal.destroy().then(function() {
259+
res.json({success: 'Goal undone'});
260+
});
261+
}
262+
});
263+
}
264+
});
265+
});

‎app/controllers/home.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
var express = require('express'),
2+
router = express.Router(),
3+
db = require('../models');
4+
5+
module.exports = function (app) {
6+
app.use('/', router);
7+
};
8+
9+
router.get('/', function (req, res, next) {
10+
res.render('index', {title: 'BN Foosball Score Tracker'});
11+
});
12+
router.get('/history', function (req, res, next) {
13+
// TODO: get score of last game
14+
// Maybe graph?
15+
res.render('history', {title: 'Score History :: BN Foosball Score Tracker'});
16+
});
17+
18+
/*db.Game.findAll().success(function (games) {
19+
res.render('index', {
20+
title: 'Foosball Score Tracker',
21+
games: games
22+
});
23+
});*/

‎app/controllers/player.js

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
var express = require('express'),
2+
router = express.Router(),
3+
db = require('../models');
4+
5+
module.exports = function (app) {
6+
app.use('/players', router);
7+
};
8+
9+
router.get('/all', function (req, res, next) {
10+
db.Player.findAll().success(function (players) {
11+
var ret = {players: []};
12+
var details = {};
13+
players.forEach(function(player) {
14+
details = {id: player.id, name: player.name, email: player.email };
15+
ret.players.push(details);
16+
});
17+
res.json(ret);
18+
});
19+
});
20+
router.get('/:id', function (req, res, next) {
21+
db.Player.find({ where: { id: req.params.id }}).success(function (player) {
22+
// Get Player's last game?
23+
if (player) res.json(player);
24+
else res.json({});
25+
});
26+
});
27+
28+
// Update a player's name (email cannot change)
29+
router.put('/:id', function(req, res, next) {
30+
db.Player.find({ where: { id: req.params.id, email: req.body.email }}).then(function (player) {
31+
if (!player)
32+
{
33+
res.json({error: 'You cannot update an email address, just a name. Create a new user for each email.'});
34+
}
35+
else
36+
{
37+
player.updateAttributes({ name: req.body.name }).then(function() {
38+
res.json(player);
39+
});
40+
}
41+
});
42+
});
43+
44+
// Create
45+
router.post('/create', function(req, res, next) {
46+
db.Player.find({ where: { email: req.body.email }}).then(function (player) {
47+
if (player)
48+
{
49+
res.json({error: 'Email address already used'});
50+
}
51+
else
52+
{
53+
db.Player.create({name: req.body.name, email: req.body.email}).then(function(player) {
54+
res.json(player);
55+
});
56+
}
57+
});
58+
});
59+
60+
// Delete a player
61+
router.delete('/:id', function(req, res, next) {
62+
db.Player.find({ where: { id: req.params.id }}).then(function (player) {
63+
if (!player)
64+
{
65+
res.json({error: 'Player not found with that ID'});
66+
}
67+
else
68+
{
69+
player.destroy().then(function() {
70+
res.json({success: true});
71+
});
72+
}
73+
});
74+
});

‎app/models/games.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Games history
3+
*/
4+
module.exports = function (sequelize, DataTypes) {
5+
6+
var Game = sequelize.define('Game', {
7+
when: DataTypes.DATE,
8+
threshold: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 5}
9+
}, {
10+
classMethods: {
11+
associate: function (models) {
12+
Game.belongsToMany(models.Player, { through: 'GamePlayers' });
13+
Game.hasMany(models.Goal);
14+
}
15+
}
16+
});
17+
18+
return Game;
19+
};
20+

‎app/models/goals.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Goals scored in a single game
3+
*/
4+
module.exports = function (sequelize, DataTypes) {
5+
6+
var Goal = sequelize.define('Goal', {
7+
when: DataTypes.DATE
8+
}, {
9+
classMethods: {
10+
associate: function (models) {
11+
Goal.belongsTo(models.Player);
12+
Goal.belongsTo(models.Game);
13+
}
14+
}
15+
});
16+
17+
return Goal;
18+
};
19+

‎app/models/index.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
var fs = require('fs'),
2+
path = require('path'),
3+
Sequelize = require('sequelize'),
4+
config = require('../../config/config'),
5+
db = {};
6+
7+
var sequelize = new Sequelize(config.db, {
8+
storage: config.storage
9+
});
10+
11+
fs.readdirSync(__dirname).filter(function (file) {
12+
return (file.indexOf('.') !== 0) && (file !== 'index.js');
13+
}).forEach(function (file) {
14+
var model = sequelize['import'](path.join(__dirname, file));
15+
db[model.name] = model;
16+
});
17+
18+
Object.keys(db).forEach(function (modelName) {
19+
if ('associate' in db[modelName]) {
20+
db[modelName].associate(db);
21+
}
22+
});
23+
24+
db.sequelize = sequelize;
25+
db.Sequelize = Sequelize;
26+
27+
module.exports = db;

‎app/models/players.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Players for the game score history
3+
*/
4+
module.exports = function (sequelize, DataTypes) {
5+
var Player = sequelize.define('Player', {
6+
name: DataTypes.STRING,
7+
email: DataTypes.STRING
8+
}, {
9+
classMethods: {
10+
associate: function (models) {
11+
Player.belongsToMany(models.Game, { through: 'GamePlayers' });
12+
}
13+
}
14+
});
15+
16+
return Player;
17+
};
18+

‎app/views/error.swig

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{% extends 'layout.swig' %}
2+
3+
{% block content %}
4+
<h1>{{ message }}</h1>
5+
<h2>{{ error.status }}</h2>
6+
<pre>{{ error.stack }}</pre>
7+
{% endblock %}

‎app/views/game.swig

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{% extends 'layout.swig' %}
2+
3+
{% block scripts %}
4+
<script>
5+
var gameStart = new Date(Date.parse('{{ gameStart.toISOString() }}'));
6+
var gameThreshold = {{gameThreshold}};
7+
var gameId = {{gameId}};
8+
var game = {};
9+
</script>
10+
<script src="/js/main.js"></script>
11+
<script src="/js/game.js"></script>
12+
<script>
13+
$(document).on('ready', function() {
14+
initGamePage();
15+
});
16+
</script>
17+
{% endblock %}
18+
19+
{% block content %}
20+
<h1>Let's Play FOOSBALL!</h1>
21+
22+
<div class="ui center aligned equal width grid">
23+
<div id="scoreboard" class="ui row"></div>
24+
</div>
25+
26+
<div id="players" class="ui center aligned equal width grid">
27+
<div class="ui row"><div class="ui column"><h2 class="ui header teal">Players</h2></div></div>
28+
29+
<ul id="players-list" class="ui row"></ul>
30+
31+
<div class="ui row controls">
32+
<div class="ui column">
33+
<select class="ui dropdown">
34+
<option value="0">Select a Player...</option>
35+
</select>
36+
<button class="ui large button" onclick="joinGame();" id="player-game-join">Join Game</button>
37+
</div>
38+
</div>
39+
</div>
40+
<div id="winner" class="ui inverted teal segment center aligned"></div>
41+
42+
<div id="start-time" class="ui inverted segment footer">{{ gameStart.toString() }}</div>
43+
{% endblock %}

‎app/views/index.swig

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{% extends 'layout.swig' %}
2+
3+
{% block content %}
4+
<h1>{{ title }}</h1>
5+
<div id="messages">
6+
{% for msg in messages %}
7+
{% if loop.first %}<ul>{% endif %}
8+
<li>{{ msg }}</li>
9+
{% if loop.last %}</ul>{% endif %}
10+
{% endfor %}
11+
</div>
12+
<div class="ui segment basic">
13+
<button class="ui button" onclick="newGame();">Start a New Game</button>
14+
</div>
15+
16+
<div class="ui segment basic" id="last-game">
17+
<button class="ui tiny button" onclick="loadLastGame();" title="Last Game">L</button>
18+
<button class="ui tiny button" onclick="loadGameDay();" title="Last Game Day">GD</button>
19+
<button class="ui tiny button" onclick="loadWeek();" title="Last Week">W</button>
20+
</div>
21+
{% endblock %}
22+
23+
{% block scripts %}
24+
<script src="/js/main.js"></script>
25+
<script src="/js/history.js"></script>
26+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
27+
<script>
28+
$(document).on('ready', function() { initHomePage(); });
29+
</script>
30+
{% endblock %}

‎app/views/layout.swig

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width">
6+
<title>{{ title }}</title>
7+
<link rel="stylesheet" href="/css/style.css">
8+
<link rel="stylesheet" href="/semantic.min.css">
9+
{% if ENV_DEVELOPMENT %}
10+
<script src="http://localhost:35729/livereload.js"></script>
11+
{% endif %}
12+
</head>
13+
<body>
14+
<div class="ui container">
15+
{% block content %}{% endblock %}
16+
</div>
17+
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
18+
<script src="/semantic.min.js"></script>
19+
{% block scripts %}{% endblock %}
20+
</body>
21+
</html>

‎bower.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "foos-tracker",
3+
"version": "0.0.1",
4+
"ignore": [
5+
"**/.*",
6+
"node_modules",
7+
"components"
8+
]
9+
}

‎config/config.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
var path = require('path'),
2+
rootPath = path.normalize(__dirname + '/..'),
3+
env = process.env.NODE_ENV || 'development';
4+
5+
var config = {
6+
development: {
7+
root: rootPath,
8+
app: {
9+
name: 'foos-tracker'
10+
},
11+
port: 3000,
12+
db: 'sqlite://localhost/foos-tracker-development',
13+
storage: rootPath + '/data/foos-tracker-development'
14+
},
15+
16+
test: {
17+
root: rootPath,
18+
app: {
19+
name: 'foos-tracker'
20+
},
21+
port: 3000,
22+
db: 'sqlite://localhost/foos-tracker-test',
23+
storage: rootPath + '/data/foos-tracker-test'
24+
},
25+
26+
production: {
27+
root: rootPath,
28+
app: {
29+
name: 'foos-tracker'
30+
},
31+
port: 3000,
32+
db: 'sqlite://localhost/foos-tracker-production',
33+
storage: rootPath + 'data/foos-tracker-production'
34+
}
35+
};
36+
37+
module.exports = config[env];

‎config/express.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
var express = require('express');
2+
var glob = require('glob');
3+
4+
var favicon = require('serve-favicon');
5+
var logger = require('morgan');
6+
var cookieParser = require('cookie-parser');
7+
var bodyParser = require('body-parser');
8+
var compress = require('compression');
9+
var methodOverride = require('method-override');
10+
var swig = require('swig');
11+
12+
module.exports = function(app, config) {
13+
app.engine('swig', swig.renderFile);
14+
app.set('views', config.root + '/app/views');
15+
app.set('view engine', 'swig');
16+
17+
var env = process.env.NODE_ENV || 'development';
18+
app.locals.ENV = env;
19+
app.locals.ENV_DEVELOPMENT = env == 'development';
20+
21+
// app.use(favicon(config.root + '/public/img/favicon.ico'));
22+
app.use(logger('dev'));
23+
app.use(bodyParser.json());
24+
app.use(bodyParser.urlencoded({
25+
extended: true
26+
}));
27+
app.use(cookieParser());
28+
app.use(compress());
29+
app.use(express.static(config.root + '/public'));
30+
app.use(methodOverride());
31+
32+
var controllers = glob.sync(config.root + '/app/controllers/*.js');
33+
controllers.forEach(function (controller) {
34+
require(controller)(app);
35+
});
36+
37+
app.use(function (req, res, next) {
38+
var err = new Error('Not Found');
39+
err.status = 404;
40+
next(err);
41+
});
42+
43+
if(app.get('env') === 'development'){
44+
app.use(function (err, req, res, next) {
45+
res.status(err.status || 500);
46+
res.render('error', {
47+
message: err.message,
48+
error: err,
49+
title: 'error'
50+
});
51+
});
52+
}
53+
54+
app.use(function (err, req, res, next) {
55+
res.status(err.status || 500);
56+
res.render('error', {
57+
message: err.message,
58+
error: {},
59+
title: 'error'
60+
});
61+
});
62+
63+
};

‎gulpfile.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
var gulp = require('gulp'),
2+
nodemon = require('gulp-nodemon'),
3+
livereload = require('gulp-livereload'),
4+
sass = require('gulp-ruby-sass');
5+
6+
gulp.task('sass', function () {
7+
return sass('./public/css/')
8+
.pipe(gulp.dest('./public/css'))
9+
.pipe(livereload());
10+
});
11+
12+
gulp.task('watch', function() {
13+
gulp.watch('./public/css/*.scss', ['sass']);
14+
});
15+
16+
gulp.task('develop', function () {
17+
livereload.listen();
18+
nodemon({
19+
script: 'app.js',
20+
ext: 'js coffee swig',
21+
}).on('restart', function () {
22+
setTimeout(function () {
23+
livereload.changed(__dirname);
24+
}, 500);
25+
});
26+
});
27+
28+
gulp.task('default', [
29+
'sass',
30+
'develop',
31+
'watch'
32+
]);

‎package.json

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "foos-tracker",
3+
"version": "0.0.1",
4+
"private": true,
5+
"scripts": {
6+
"start": "node app.js"
7+
},
8+
"dependencies": {
9+
"express": "~4.12.0",
10+
"serve-favicon": "~2.2.0",
11+
"morgan": "~1.5.0",
12+
"cookie-parser": "~1.3.3",
13+
"body-parser": "~1.12.0",
14+
"compression": "~1.4.1",
15+
"method-override": "~2.3.0",
16+
"glob": "~5.0.3",
17+
"sequelize": "~2.0.5",
18+
"sqlite3": "~3.0.5",
19+
"swig": "~1.4.2"
20+
},
21+
"devDependencies": {
22+
"better-console": "^0.2.4",
23+
"del": "^1.2.1",
24+
"extend": "^3.0.0",
25+
"gulp": "~3.8.10",
26+
"gulp-autoprefixer": "^2.3.1",
27+
"gulp-chmod": "^1.2.0",
28+
"gulp-clone": "^1.0.0",
29+
"gulp-concat": "^2.6.0",
30+
"gulp-concat-css": "^2.2.0",
31+
"gulp-copy": "0.0.2",
32+
"gulp-flatten": "^0.1.1",
33+
"gulp-header": "^1.2.2",
34+
"gulp-help": "^1.6.0",
35+
"gulp-if": "^1.2.5",
36+
"gulp-less": "^3.0.3",
37+
"gulp-livereload": "~3.8.0",
38+
"gulp-minify-css": "^1.2.0",
39+
"gulp-nodemon": "~2.0.2",
40+
"gulp-notify": "^2.2.0",
41+
"gulp-plumber": "^1.0.1",
42+
"gulp-print": "^1.1.0",
43+
"gulp-rename": "^1.2.2",
44+
"gulp-replace": "^0.5.4",
45+
"gulp-rtlcss": "^0.1.4",
46+
"gulp-ruby-sass": "~1.0.1",
47+
"gulp-uglify": "^1.2.0",
48+
"gulp-util": "^3.0.6",
49+
"gulp-watch": "^4.3.4",
50+
"map-stream": "0.0.6",
51+
"require-dot-file": "^0.4.0",
52+
"semantic-ui": "^2.0.8",
53+
"yamljs": "^0.2.3"
54+
}
55+
}

‎public/css/style.css

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
body {
2+
padding: 50px;
3+
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; }
4+
5+
a {
6+
color: #00B7FF; }
7+
8+
#winner { display: none; }

‎public/css/style.scss

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
body {
2+
padding: 50px;
3+
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4+
}
5+
6+
a {
7+
color: #00B7FF;
8+
}

‎public/js/game.js

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Fill up the players-list with the current players of this game,
3+
* and add their GOAL buttons, then add the remaining players to the
4+
* controls for addition to this game. Finally initialize the current
5+
* score for the game.
6+
*
7+
* NOTE: This method should only be called once the document is ready.
8+
*/
9+
function initGamePage()
10+
{
11+
$.get('/players/all/', {}, function(data, text, xhr) {
12+
var allPlayers = data.players;
13+
$.get('/games/' + gameId, {}, function(data, text, xhr) {
14+
// Player info
15+
allPlayers.forEach(function(player) {
16+
if (data.players[player.id])
17+
addActivePlayer(player);
18+
else
19+
addAvailablePlayer(player);
20+
});
21+
22+
// Current Game info
23+
refreshGameInfo();
24+
});
25+
});
26+
27+
// Init our Semantic UI dropdown
28+
$('.ui.dropdown').dropdown();
29+
}
30+
31+
/**
32+
* Helper method to add an active player for the Game Init method.
33+
* Includes their "Goal" button, and placeholder on the Scoreboard.
34+
*/
35+
function addActivePlayer(player)
36+
{
37+
var button = '<button class="ui button" onclick="score(' + player.id + ');">Goal!</button>';
38+
$('#players-list').append('<li class="ui column"><p>' + player.name + '</p>' + button + '</li>');
39+
40+
// Add player to the Scoreboard
41+
$('#scoreboard').append('<div class="ui column" id="player-score-' + player.id + '">' +
42+
'<h2>0</h2><p>' + player.name + '</p>' +
43+
'</div>');
44+
45+
// Add player score to memory
46+
game[player.id] = 0;
47+
}
48+
49+
/**
50+
* Helper method to add an available player to the dropdown list.
51+
*/
52+
function addAvailablePlayer(player)
53+
{
54+
var opt = '<option id="player-available-' + player.id + '" value="' + player.id + '">' + player.name + '</option>';
55+
$('#players .controls select').append(opt);
56+
}
57+
58+
/**
59+
* Add the currently selected available player to the game.
60+
*/
61+
function joinGame()
62+
{
63+
var pid = $('#players .controls select').val();
64+
if (pid <= 0) return;
65+
66+
$.get('/players/' + pid, {}, function(player, text, xhr) {
67+
$.post('/games/' + gameId + '/add/player/' + pid, {}, function(data, text, xhr) {
68+
addActivePlayer(player);
69+
// Success! Remove from available list
70+
$('#player-available-' + pid).remove();
71+
});
72+
});
73+
}
74+
75+
/**
76+
* Score a goal.
77+
*/
78+
function score(pid)
79+
{
80+
$.post('/games/' + gameId + '/goal/player/' + pid, {}, function(data, text, xhr) {
81+
// Update Scoreboard
82+
game[pid]++;
83+
$('#player-score-' + pid).children('h2').replaceWith('<h2>' + game[pid] + '</h2>');
84+
85+
// We have a winner (verify by talking to the server)
86+
if (game[pid] >= gameThreshold) refreshGameInfo();
87+
});
88+
}
89+
90+
/**
91+
* Get the game info. (Display winner if anyone has > 5 goals)
92+
*/
93+
function refreshGameInfo()
94+
{
95+
$.get('/games/' + gameId, {}, function(data, text, xhr) {
96+
var winner = null;
97+
for (pid in data.players)
98+
{
99+
// Check for a winner
100+
if (data.players[pid].goals >= data.threshold)
101+
{
102+
// Just in case threshold was set wrong
103+
if (!winner || data.players[pid].goals > winner.goals)
104+
winner = data.players[pid];
105+
}
106+
107+
// Update player score
108+
game[pid] = data.players[pid].goals;
109+
$('#player-score-' + pid).children('h2').replaceWith('<h2>' + game[pid] + '</h2>');
110+
}
111+
112+
// Close the game out
113+
if (winner) showWinner(winner);
114+
});
115+
}
116+
117+
/**
118+
* Display the winner of this game, and button to create a new game.
119+
*/
120+
function showWinner(winner)
121+
{
122+
$('#winner').append('<h2>' + winner.name + ' Crushed It!!!</h2>');
123+
// Disable all goal buttons
124+
$('#players-list li button').prop("disabled", true);
125+
// Disable Join button
126+
$('#players .controls button').prop("disabled", true);
127+
128+
// New game button
129+
$('#winner').append('<button class="ui massive button" onclick="newGame();">Play a new Game</button>');
130+
$('#winner').fadeIn();
131+
}

‎public/js/history.js

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
var gameHistory = {};
2+
3+
/**
4+
* Display the pie chart for the most recently played game.
5+
*/
6+
function initHomePage()
7+
{
8+
$.get('/games/last-week', {}, function(data, text, xhr) {
9+
// Put a link to the most recent game
10+
if (data && data.games)
11+
{
12+
if (data.games.length == 0)
13+
$('#last-game').append('<h2>No Game History!</h2>');
14+
else
15+
{
16+
var when = new Date(Date.parse(data.games[0].when));
17+
var h = when.getHours();
18+
var m = when.getMinutes() < 10 ? "0" + when.getMinutes() : when.getMinutes();
19+
var time = (h > 12) ? (h - 12) + ':' + m + 'pm' : h + ':' + m + 'am';
20+
var date = getDay(when) + ' @ ' + time;
21+
$('#last-game').append('<h2><a href="/game/' + data.games[0].id + '">' + date + '</h2>');
22+
23+
// Get most recent data into memory
24+
gameHistory = data.games;
25+
26+
// And kick off the D3 rendering of the pie chart
27+
setupVisualization();
28+
}
29+
}
30+
});
31+
}
32+
33+
// Global namespace for caching some of the configured d3 settings
34+
var viz = {};
35+
36+
/**
37+
* Setup visualization. Using data from the most recent game.
38+
*/
39+
function setupVisualization()
40+
{
41+
var width = 500, height = 350;
42+
var radius = Math.min(width, height) / 2;
43+
44+
// Define our palette
45+
var color = d3.scale.ordinal().range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
46+
47+
var outer = radius - 10;
48+
viz.arc = d3.svg.arc().outerRadius(outer).innerRadius(0);
49+
viz.textarc = d3.svg.arc().outerRadius(outer).innerRadius(outer / 3);
50+
51+
viz.pie = d3.layout.pie()
52+
// default sort is descending order
53+
// Grab data using the "goals" property on a player
54+
.value(function(d) { return d.goals; });
55+
56+
var svg = d3.select("#last-game").append("svg")
57+
.attr("width", width)
58+
.attr("height", height)
59+
.append("g")
60+
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
61+
62+
// Load data from the most recent game in the gameHistory global, (or nothing if empty)
63+
// D3 needs an array of values that can be read using the FUNC from
64+
// "d3.layout.pie().value(FUNC)"
65+
var data = (gameHistory) ? gameHistory[0].players : [];
66+
67+
var g = svg.selectAll()
68+
.data(viz.pie(data))
69+
.enter().append("g")
70+
.attr("class", "arc");
71+
72+
g.append("path")
73+
.attr("d", viz.arc)
74+
.style("fill", function(d) { return color(d.data.name); });
75+
76+
var label = g.append("g")
77+
.attr("transform", function(d) { return "translate(" + viz.textarc.centroid(d) + ")"; })
78+
.attr("id", function(d) { return "player-arc-" + d.data.id; } )
79+
.style("text-anchor", "middle");
80+
label.append("text")
81+
.style("font-weight", "bold")
82+
.attr("class", "name")
83+
.text(function(d) { return (d.data.goals > 0) ? d.data.name : ""; });
84+
label.append("text")
85+
.attr("dy", "1.1em")
86+
.attr("class", "goals")
87+
.text(function(d) { return (d.data.goals > 0) ? d.data.goals : ""; });
88+
}
89+
90+
/**
91+
* Load data into the graph from the last game.
92+
*/
93+
function loadLastGame()
94+
{
95+
if (!gameHistory) return;
96+
var gameDay = new Date(Date.parse(gameHistory[0].when));
97+
var h = gameDay.getHours();
98+
var m = gameDay.getMinutes() < 10 ? "0" + gameDay.getMinutes() : gameDay.getMinutes();
99+
var time = (h > 12) ? (h - 12) + ':' + m + 'pm' : h + ':' + m + 'am';
100+
var date = getDay(gameDay) + ' @ ' + time;
101+
$('#last-game h2').replaceWith('<h2><a href="/game/' + gameHistory[0].id + '">' + date + '</a></h2>');
102+
103+
changeData(first(gameHistory));
104+
}
105+
106+
/**
107+
* Load data into graph for the last "game day".
108+
*/
109+
function loadGameDay()
110+
{
111+
if (!gameHistory) return;
112+
var gameDay = new Date(Date.parse(gameHistory[0].when));
113+
$('#last-game h2').replaceWith('<h2>' + getDay(gameDay) + '</h2>');
114+
115+
// Sum all data from the same y/m/d of the latest game in our gameHistory
116+
var data = first(gameHistory);
117+
for (var i = 1; i < gameHistory.length; i++)
118+
{
119+
var day = new Date(Date.parse(gameHistory[i].when));
120+
if (!(day.getYear() == gameDay.getYear() &&
121+
day.getMonth() == gameDay.getMonth() &&
122+
day.getDay() == gameDay.getDay()))
123+
continue;
124+
for (var pidx = 0; pidx < data.length; pidx++)
125+
{
126+
data[pidx].goals += gameHistory[i].players[pidx].goals;
127+
}
128+
}
129+
130+
changeData(data);
131+
}
132+
133+
/**
134+
* Get a day as a string.
135+
*/
136+
function getDay(d)
137+
{
138+
var date = d.getFullYear() + ' / ' + d.getMonth() + ' / ' + d.getDay()
139+
return date;
140+
}
141+
142+
/**
143+
* Load data into graph for the last "week" (our full gameHistory).
144+
*/
145+
function loadWeek()
146+
{
147+
if (!gameHistory) return;
148+
149+
var gameDay = new Date(Date.parse(gameHistory[0].when));
150+
var startDay = gameDay;
151+
startDay.setDate(gameDay.getDate() - 7);
152+
$('#last-game h2').replaceWith('<h2>' + getDay(startDay) + ' &mdash; ' + getDay(gameDay) + '</h2>');
153+
154+
// Sum all data from the same y/m/d of the latest game in our gameHistory
155+
var data = first(gameHistory);
156+
for (var i = 1; i < gameHistory.length; i++)
157+
for (var pidx = 0; pidx < data.length; pidx++)
158+
data[pidx].goals += gameHistory[i].players[pidx].goals;
159+
160+
changeData(data);
161+
}
162+
163+
/**
164+
* Clone the first value out of this players array.
165+
*/
166+
function first(game)
167+
{
168+
var ret = [];
169+
game[0].players.forEach(function(player) {
170+
ret.push({id: player.id, name: player.name, email: player.email, goals: player.goals});
171+
});
172+
return ret;
173+
}
174+
175+
/**
176+
*
177+
178+
/**
179+
* Change the pie chart to display the new data as specified by the parameter.
180+
*/
181+
function changeData(data)
182+
{
183+
// All current pie slices (just the paths)
184+
var sliceLayer = d3.selectAll("#last-game svg .arc");
185+
186+
// Update data to new values (on the .arc, and all children)
187+
sliceLayer.data(viz.pie(data));
188+
189+
// Create a transition
190+
var t1 = sliceLayer.transition().duration(1000);
191+
192+
// Animate the path change
193+
t1.select("path").attr("d", viz.arc);
194+
// And the text position
195+
t1.select("g").attr("transform", function(d) { return "translate(" + viz.textarc.centroid(d) + ")"; });
196+
// And text values (after the objects move)
197+
var t2 = t1.transition();
198+
t2.select("g .name").text(function(d) { return (d.data.goals > 0) ? d.data.name : ""; });
199+
t2.select("g .goals").text(function(d) { return (d.data.goals > 0) ? d.data.goals : ""; });
200+
}

‎public/js/main.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Create a new game and redirect to that game.
3+
*/
4+
function newGame()
5+
{
6+
$.post('/games/create', {}, function(data, text, xhr) {
7+
window.location = '/game/' + data.id;
8+
});
9+
}

‎public/semantic.min.css

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎public/semantic.min.js

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎semantic.json

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"base": "semantic/",
3+
"paths": {
4+
"source": {
5+
"config": "src/theme.config",
6+
"definitions": "src/definitions/",
7+
"site": "src/site/",
8+
"themes": "src/themes/"
9+
},
10+
"output": {
11+
"packaged": "../public/",
12+
"uncompressed": "public/components/",
13+
"compressed": "public/components/",
14+
"themes": "public/themes/"
15+
},
16+
"clean": "public/"
17+
},
18+
"permission": false,
19+
"rtl": "No",
20+
"components": [
21+
"reset",
22+
"site",
23+
"button",
24+
"container",
25+
"divider",
26+
"header",
27+
"icon",
28+
"image",
29+
"input",
30+
"label",
31+
"list",
32+
"loader",
33+
"segment",
34+
"form",
35+
"grid",
36+
"menu",
37+
"message",
38+
"table",
39+
"accordion",
40+
"checkbox",
41+
"dropdown",
42+
"modal",
43+
"transition",
44+
"api",
45+
"form",
46+
"state"
47+
],
48+
"version": "2.0.8"
49+
}

0 commit comments

Comments
 (0)
Please sign in to comment.