Skip to content

Instantly share code, notes, and snippets.

@jberger
Last active August 29, 2015 14:05
Show Gist options
  • Save jberger/242e60118983fa4331c8 to your computer and use it in GitHub Desktop.
Save jberger/242e60118983fa4331c8 to your computer and use it in GitHub Desktop.
A golf leaderboard websocket app
#!/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