Last active
August 29, 2015 14:05
-
-
Save jberger/242e60118983fa4331c8 to your computer and use it in GitHub Desktop.
A golf leaderboard websocket app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl | |
use Mojolicious::Lite; | |
use Mango; | |
use Mojo::IOLoop; | |
use DDP; | |
use feature 'current_sub'; | |
plugin 'Config' => { | |
file => 'leaderboard.conf', | |
defaults => { | |
secrets => ['SomeTHing'], | |
}, | |
}; | |
app->secrets(app->config->{secrets}); | |
helper mango => sub { state $mango = Mango->new('mongodb://localhost/') }; | |
helper db => sub { state $db = shift->mango->db('leaderboard') }; | |
helper players => sub { state $players = shift->db->collection('players') }; | |
helper ticker => sub { state $ticker = shift->db->collection('ticker') }; | |
helper view => sub { return sub { shift; p @_ } }; | |
helper publish_scores => sub { | |
my ($c, $cb) = @_; | |
my $delay = Mojo::IOLoop->delay( | |
sub { | |
my $delay = shift; | |
$c->players->find->all($delay->begin); | |
}, | |
sub { | |
my ($delay, $err, $players) = @_; | |
die $err if $err; | |
my $table = $c->render_to_string( 'table', players => $players ); | |
$c->ticker->insert({table => $table}, $delay->begin); | |
$delay->pass($table); | |
}, | |
sub { | |
my ($delay, $err, $oid, $table); | |
die $err if $err; | |
$c->$cb(undef, $table); | |
}, | |
); | |
$delay->catch(sub { $c->$cb($_[1], undef) }); | |
$delay->wait; | |
}; | |
helper add_card => sub { | |
my $cb = pop; | |
my ($c, $name, $start) = @_; | |
$start ||= 1; | |
my $card = { | |
name => $name, | |
current => $start, | |
scores => {}, | |
}; | |
my $delay = Mojo::IOLoop->delay( | |
sub { | |
my $delay = shift; | |
$c->players->insert($card, $delay->begin); | |
}, | |
sub { | |
my ($delay, $err, $oid) = @_; | |
die $err if $err; | |
$c->$cb(undef, $card); | |
}, | |
); | |
$delay->catch(sub{ $c->$cb($_[1], undef) }); | |
$delay->wait; | |
}; | |
helper get_card => sub { | |
my ($c, $name, $cb) = @_; | |
my $delay = Mojo::IOLoop->delay( | |
sub { | |
my $delay = shift; | |
$c->players->find_one({ name => $name }, $delay->begin); | |
}, | |
sub { | |
my ($delay, $err, $card) = @_; | |
$c->$cb($err, $card); | |
}, | |
); | |
$delay->catch(sub{ $c->$cb($_[1], undef) }); | |
$delay->wait; | |
}; | |
helper set_score => sub { | |
my ($c, $name, $hole, $score, $cb) = @_; | |
my $delay = Mojo::IOLoop->delay( | |
sub { | |
my $delay = shift; | |
$c->get_card($name, $delay->begin); | |
}, | |
sub { | |
my ($delay, $err, $card) = @_; | |
die $err if $err; | |
$card->{scores}{$hole} = $score; | |
$card->{current} = next_hole($hole); | |
$delay->data(card => $card); | |
$c->players->save($card, $delay->begin); | |
}, | |
sub { | |
my ($delay, $err, $oid) = @_; | |
die $err if $err; | |
$c->$cb(undef, $delay->data->{card}); | |
}, | |
); | |
$delay->catch(sub{ $c->$cb($_[1], undef) }); | |
$delay->wait; | |
}; | |
sub next_hole { ($_[0] % 18) + 1 } | |
get '/' => sub { | |
my $c = shift; | |
my $name = $c->session('name'); | |
return $c->render('login') unless $name; | |
$c->stash(name => $name); | |
$c->delay( | |
sub { | |
my $delay = shift; | |
$c->get_card($name, $delay->begin); | |
$c->players->find->all($delay->begin); | |
}, | |
sub { | |
my ($self, $card_err, $card, $players_err, $players) = @_; | |
die $card_err if $card_err; | |
die $players_err if $players_err; | |
$c->render('index', card => $card, players => $players); | |
}, | |
); | |
}; | |
post '/do_login' => sub { | |
my $c = shift; | |
my $name = $c->param('name'); | |
$c->render_later; | |
$c->delay( | |
sub { | |
my $delay = shift; | |
$c->get_card($name, $delay->begin); | |
}, | |
sub { | |
my ($delay, $err, $card) = @_; | |
die $err if $err; | |
if ($card) { | |
$delay->pass(undef, $card); | |
} else { | |
my $hole = $c->param('hole') || 1; | |
$c->add_card($name, $hole, $delay->begin); | |
} | |
}, | |
sub { | |
my ($delay, $err, $card) = @_; | |
die $err if $err; | |
$c->publish_scores($delay->begin); | |
}, | |
sub { | |
my ($delay, $err, $html) = @_; | |
die $err if $err; | |
$c->session(name => $name); | |
$c->redirect_to('/'); | |
}, | |
); | |
}; | |
any '/logout' => sub { | |
my $c = shift; | |
$c->session( expires => 1 ); | |
$c->redirect_to('/'); | |
}; | |
websocket '/submit_score' => sub { | |
my $c = shift; | |
$c->inactivity_timeout(60*60*24); | |
$c->on( json => sub { | |
my ($ws, $data) = @_; | |
$ws->delay( | |
sub { | |
my $delay = shift; | |
$ws->set_score($data->{name}, $data->{hole}, $data->{score}, $delay->begin); | |
}, | |
sub { | |
my ($delay, $err, $card) = @_; | |
die $err if $err; | |
$ws->send({ json => { hole => $card->{current} } }); | |
$ws->publish_scores($delay->begin); | |
}, | |
sub { | |
my ($delay, $err, $html) = @_; | |
die $err if $err; | |
}, | |
); | |
}); | |
my $stop = 0; | |
my $cursor = $c->ticker->find->tailable(1)->await_data(1); | |
$cursor->next(sub{ | |
my ($cursor, $err, $data) = @_; | |
return if $err; | |
$c->send({ json => $data }) if $data; | |
$cursor->next(__SUB__) unless $stop; | |
}); | |
$c->on( finish => sub { $stop++ } ); | |
}; | |
app->start; | |
__DATA__ | |
@@ layouts/basic.html.ep | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title><%= title %></title> | |
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"> | |
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> | |
</head> | |
<body> | |
<div class="container"> | |
<%= content %> | |
</div> | |
%= javascript begin | |
var ws; | |
function submitScore () { | |
var name = $('#name'); | |
var hole = $('#hole'); | |
var score = $('#score'); | |
ws.send(JSON.stringify({'name':name.val(), 'hole': hole.val(), 'score': score.val()})); | |
hole.val(''); | |
score.val(''); | |
} | |
$(function(){ | |
if (!("WebSocket" in window)) { | |
alert('Your browser does not support WebSockets!'); | |
return; | |
} | |
ws = new WebSocket("<%= url_for('submit_score')->to_abs %>"); | |
ws.onmessage = function (evt) { | |
var data = JSON.parse(evt.data); | |
if (data.hasOwnProperty("table")) { | |
$('#table').html(data.table); | |
} | |
if (data.hasOwnProperty("hole")) { | |
$('#hole').val(data.hole); | |
} | |
}; | |
}) | |
%= end | |
</body> | |
</html> | |
@@ login.html.ep | |
% title 'Login'; | |
% layout 'basic'; | |
<form action="/do_login" method="POST" role="form"> | |
<div class="form-group"> | |
<label for="name">Name</label> | |
<input type="text" class="form-control" name="name" id="name"> | |
</div> | |
<div class="form-group"> | |
<label for="hole">Starting On Hole</label> | |
<input type="number" min="1" max="18" class="form-control" name="hole" id="hole" value="1"> | |
</div> | |
<input type="submit" class="btn"> | |
</form> | |
@@ index.html.ep | |
% title 'Leaderboard'; | |
% layout 'basic'; | |
<div class="form-inline" role="form"> | |
<div class="form-group"> | |
<label for="name">Name</label> | |
<input type="text" class="form-control" name="name" id="name" value="<%= $name %>" readonly> | |
</div> | |
<div class="form-group"> | |
<label for="hole">Hole</label> | |
<input type="number" min="1" max="18" class="form-control" name="hole" id="hole" value="<%= $card->{current} %>"> | |
</div> | |
<div class="form-group"> | |
<label for="score">Score</label> | |
<input type="number" min="1" max="15" class="form-control" name="score" id="score"> | |
</div> | |
<input type="submit" value="Submit" class="btn btn-primary" onclick="submitScore()"> | |
<a href="<%= url_for('logout') %>" class="pull-right btn btn-info" role="button">Logout</a> | |
</div> | |
<div style="min-height: 30px;"></div> | |
<div class="row"> | |
<div class="col-md-12"> | |
<table class="table table-bordered table-condensed"> | |
<thead> | |
<tr> | |
<th>Name</th> | |
% for my $hole (1..18) { | |
%= t th => $hole | |
% } | |
<th>Total</th> | |
</tr> | |
</thead> | |
<tbody id="table"> | |
%= include 'table' | |
</tbody> | |
</table> | |
</div> | |
</div> | |
@@ table.html.ep | |
% use List::Util 'sum'; | |
% no warnings 'numeric'; | |
% foreach my $row (@$players) { | |
<tr> | |
<td><%= $row->{name} %></td> | |
% foreach my $hole (1..18) { | |
<td><%= $row->{scores}{$hole} || '' %></td> | |
% } | |
% my $total = sum values %{$row->{scores}}; | |
<td><%= $total %></td> | |
</tr> | |
% } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment