Hack24 2017

Today’s header image was created by Paul Seal, and it was taken as AbstractSausageFactory took an early morning break while hacking together their entry for Hack24.
This week’s post will be a slight departure from .NET Core stuff… kinda
Hack24 The Planet!
Not quite.
This past weekend, I took part in Hack24 in Nottingham with Zac Braddy, Paul Seal and James Studdart; we entered the competition as AbstractSausageFactory and, in less than 24 hours, created an entire line of business application from nothing.
An Entire Application?
Yup. We started work at 12 midday on the Saturday and by 12 midday on the Sunday we had an entire application which could measure employee engagement.
The event itself actually started at 9 am on the Saturday
It used an impressive list of technologies too:
- GitHub integration
- .NET Core
- Node.js
- ESLint
- .NET Framework
- ReactJS
- Firebase
That’s an end-to-end description of the technologies used in the stack.
Although, I’m sure that I’m leaving something out.
Zac has already published a blog post about the things that he learnt with regards to ReactJS over at thereactionary.net. I’d recommend that you give it a read, as there’s a lot of distilled knowledge in that post.
As with his other posts too, of course
The challenge we had entered called for us to measure employee engagement across a fictional company. I’ll let MHR (the challenge sponsor) describe it best:

Based on that brief we set about creating an application which would measure engagement via work completed.
If you to skip directly to our submission video, you can do so by clicking here.
The Idea
We started where we live: programming.
Let’s make programming into a game.
We started with by taking a look at Code.RPGify to see if we could figure out how that worked and whether we could rip it off gain inspiration from it.
If we could gamify cutting and committing code, then we could produce stats on it. We could then provide an admin panel and let managers and tech leads see how their developers are doing with their tasks.
We retired to a safe distance (Starbucks across the street), white board in hand, and created the first of our design diagrams.

We had a simple idea:
- User commits code to GitHub
- GitHub talks to an API that I would write (in .NET Core)
Because it’s important to be relevant to this blog, right?
- My API would pass on relevant information to Zac’s Node instance
- Zac’s Node instance would do some magic and send data to James’ game server
- James’ game server would send some data to Paul’s Firebase DB server
There’s a lot of the magic missing here, because you’re only interested in the parts I put together, right?
My API
I needed to know how to communicate with GitHub, so I looked up their version of GitHooks.
GitHub is just Git in the cloud, so it should have GitHooks, right?
Turns out that GitHub has a thing called Webhooks. A GitHub repo can be configured to send messages to a given external API when certain events happen. These events are:
- A repository is pushed to
- A pull request is opened
- A GitHub Pages site is built
- A new member is added to a team
The only one we were interested in was a repo push. So I navigated to a GitHub repo and set one up. It was surprisingly easy to do.
Setting up a new GitHub Webhook
I headed over to the settings page of the GitHub repo that I was looking at, found the big “Webhooks” button, and gave it a click.

A Webhook will send you either JSON data or a urlencoded web-form. I chose to receive web-form data, that way the data would be sent to my API in a string called “payload” – making parsing it a little easier.
The WebApi Project
Then I threw together a .NET Core WebApi application which would act as the POST end point for GitHub to communicate with. I spun up an Azure instance
I’ve written about spinning one of those up before, but as a cloud VM
and published to it.
To test that it was working (after adding a single GET method), I fired up postman and sent it a GET request.

Now that it was responding to GET requests, I needed to get it to receive and parse JSON data as part of a POST and I would be set.
Parsing the JSON Data
Since the data would be in JSON, I threw together some classes which represented the JSON data and decorated the properties with JsonProperty attributes.
I didn’t have time to represent every property of the JSON, so I picked the most important ones for our application
We only had 24 hours, after all
I threw the following classes together:
namespace asti.GitHubHookApi.Models | |
{ | |
public class GitHubPushJson | |
{ | |
[JsonProperty("ref")] | |
public string Ref { get; set; } | |
[JsonProperty("compare")] | |
public string CompareUrl { get; set; } | |
[JsonProperty("after")] | |
public string AfterSha { get; set; } | |
[JsonProperty("pusher")] | |
public PullPusherJson Pusher { get; set; } | |
[JsonProperty("commits")] | |
public ICollection<PushCommitJson> Commits { get; set; } | |
[JsonProperty("head_commit")] | |
public PushHeadCommitJson HeadCommit { get; set; } | |
[JsonProperty("repository")] | |
public PullRepositoryJson Repository { get; set; } | |
public string CommitsApiUrl() | |
{ | |
return Repository != null ? Repository.CommitsUrl.Replace("{/sha}", $"/{AfterSha}") : string.Empty; | |
} | |
/* | |
"pusher": { | |
"name": "baxterthehacker", | |
"email": "[email protected]" | |
}, | |
*/ | |
/// <summary> | |
/// Represents the user who pushed the commit to a push JSON | |
/// (all fields mapped) | |
/// https://developer.github.com/v3/activity/events/types/#pushevent | |
/// </summary> | |
public class PullPusherJson | |
{ | |
[JsonProperty("name")] | |
public string Name { get; set; } | |
[JsonProperty("email")] | |
public string Email { get; set; } | |
} | |
/* | |
{ | |
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", | |
"tree_id": "f9d2a07e9488b91af2641b26b9407fe22a451433", | |
"distinct": true, | |
"message": "Update README.md", | |
"timestamp": "2015-05-05T19:40:15-04:00", | |
"url": "https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", | |
"author": { | |
"name": "baxterthehacker", | |
"email": "[email protected]", | |
"username": "baxterthehacker" | |
}, | |
"committer": { | |
"name": "baxterthehacker", | |
"email": "[email protected]", | |
"username": "baxterthehacker" | |
}, | |
"added": [ | |
], | |
"removed": [ | |
], | |
"modified": [ | |
"README.md" | |
] | |
} | |
*/ | |
/// <summary> | |
/// Represents a commit form a Push event | |
/// (not all fields represented here) | |
/// https://developer.github.com/v3/activity/events/types/#pushevent | |
/// </summary> | |
public class PushCommitJson | |
{ | |
[JsonProperty("id")] | |
public string Id { get; set; } | |
[JsonProperty("message")] | |
public string CommitMessage { get; set; } | |
[JsonProperty("timestamp")] | |
public string TimeStampAsString { get; set; } | |
[JsonProperty("url")] | |
public string CommitUrl { get; set; } | |
[JsonProperty("added")] | |
public ICollection<string> Added { get; set; } | |
[JsonProperty("removed")] | |
public ICollection<string> Removed { get; set; } | |
[JsonProperty("modified")] | |
public ICollection<string> Modified { get; set; } | |
} | |
/* | |
"head_commit": { | |
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", | |
"tree_id": "f9d2a07e9488b91af2641b26b9407fe22a451433", | |
"distinct": true, | |
"message": "Update README.md", | |
"timestamp": "2015-05-05T19:40:15-04:00", | |
"url": "https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", | |
"author": { | |
"name": "baxterthehacker", | |
"email": "[email protected]", | |
"username": "baxterthehacker" | |
}, | |
} | |
*/ | |
public class PushHeadCommitJson | |
{ | |
[JsonProperty("id")] | |
public string Id { get; set; } | |
[JsonProperty("distinct")] | |
public bool Distinct { get; set; } | |
[JsonProperty("message")] | |
public string Message { get; set; } | |
[JsonProperty("timestamp")] | |
public string TimestampAsString { get; set; } | |
[JsonProperty("url")] | |
public string Url { get; set; } | |
} | |
public class PushHeadCommitAuthor : PullPusherJson | |
{ | |
[JsonProperty("username")] | |
public string UserName { get; set; } | |
} | |
/* | |
"repository": { | |
"id": 35129377, | |
"name": "public-repo", | |
"full_name": "baxterthehacker/public-repo", | |
"owner": { | |
"name": "baxterthehacker", | |
"email": "[email protected]" | |
}, | |
"private": false, | |
"html_url": "https://github.com/baxterthehacker/public-repo", | |
"description": "", | |
"fork": false, | |
"url": "https://github.com/baxterthehacker/public-repo", | |
"forks_url": "https://api.github.com/repos/baxterthehacker/public-repo/forks", | |
"keys_url": "https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}", | |
"collaborators_url": "https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}", | |
"teams_url": "https://api.github.com/repos/baxterthehacker/public-repo/teams", | |
"hooks_url": "https://api.github.com/repos/baxterthehacker/public-repo/hooks", | |
"issue_events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}", | |
"events_url": "https://api.github.com/repos/baxterthehacker/public-repo/events", | |
"assignees_url": "https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}", | |
"branches_url": "https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}", | |
"tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/tags", | |
"blobs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}", | |
"git_tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}", | |
"git_refs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}", | |
"trees_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}", | |
"statuses_url": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}", | |
"languages_url": "https://api.github.com/repos/baxterthehacker/public-repo/languages", | |
"stargazers_url": "https://api.github.com/repos/baxterthehacker/public-repo/stargazers", | |
"contributors_url": "https://api.github.com/repos/baxterthehacker/public-repo/contributors", | |
"subscribers_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscribers", | |
"subscription_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscription", | |
"commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}", | |
"git_commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}", | |
"comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}", | |
"issue_comment_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}", | |
"contents_url": "https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}", | |
"compare_url": "https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}", | |
"merges_url": "https://api.github.com/repos/baxterthehacker/public-repo/merges", | |
"archive_url": "https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}", | |
"downloads_url": "https://api.github.com/repos/baxterthehacker/public-repo/downloads", | |
"issues_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}", | |
"pulls_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}", | |
"milestones_url": "https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}", | |
"notifications_url": "https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}", | |
"labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}", | |
"releases_url": "https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}", | |
"created_at": 1430869212, | |
"updated_at": "2015-05-05T23:40:12Z", | |
"pushed_at": 1430869217, | |
"git_url": "git://github.com/baxterthehacker/public-repo.git", | |
"ssh_url": "[email protected]:baxterthehacker/public-repo.git", | |
"clone_url": "https://github.com/baxterthehacker/public-repo.git", | |
"svn_url": "https://github.com/baxterthehacker/public-repo", | |
"homepage": null, | |
"size": 0, | |
"stargazers_count": 0, | |
"watchers_count": 0, | |
"language": null, | |
"has_issues": true, | |
"has_downloads": true, | |
"has_wiki": true, | |
"has_pages": true, | |
"forks_count": 0, | |
"mirror_url": null, | |
"open_issues_count": 0, | |
"forks": 0, | |
"open_issues": 0, | |
"watchers": 0, | |
"default_branch": "master", | |
"stargazers": 0, | |
"master_branch": "master" | |
} | |
*/ | |
public class PullRepositoryJson | |
{ | |
[JsonProperty("html_url")] | |
public string HtmlUrl { get; set; } | |
[JsonProperty("url")] | |
public string Url { get; set; } | |
[JsonProperty("commits_url")] | |
public string CommitsUrl { get; set; } | |
} | |
} | |
} |
I’ve left in comments showing the format of the original JSON data
All I had to do then was deserialise the JSON data back into an instance of the GitHubPushJson class.
[HttpPost] | |
[Route("/PushEndPoint")] | |
public async string PushEndPoint(string payload) | |
{ | |
try | |
{ | |
var pushDetails = JsonConvert.DeserializeObject<GitHubPushJson>(payload); | |
} | |
catch (Exception e) | |
{ | |
return "fail"; | |
} | |
return "parsed Ok"; | |
} |
Again, with Postman I checked that the API was working, and it was.
We Need More Data
Looking at the returned data, we didn’t really have a great deal. Namely, we had filenames, but not file paths or branch names.
Bother!
Luckily the GitHub data exposed an API that I could call to get the data for a commit.
And here’s why I left the comments in the above code
"commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}", |
Which is the same format as the GitHub Commit API method for getting all of the data for a single commit.
which you can read here
All I had to do was get the sha of the commit, replace “{sha}” with it, and put in a GET request to GitHub.
The keen eyed of you will have already spotted the method I used to get the full URL to use, earlier.
The first thing I did was write a Helper to perform the GET for me:
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net.Http; | |
using System.Threading.Tasks; | |
namespace asti.GitHubHookApi.Helpers | |
{ | |
public static class GitHubGetHelper | |
{ | |
public static async Task<string> PerformGetAsync(string urlToResource) | |
{ | |
var fullUrlWithAuth = GitHubApiUrlHelper.AddOathToApiCall(urlToResource); | |
using (var client = new HttpClient()) | |
{ | |
try | |
{ | |
var request = new HttpRequestMessage() | |
{ | |
RequestUri = fullUrlWithAuth, | |
Method = HttpMethod.Get | |
}; | |
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", | |
"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:19.0) Gecko/20100101 Firefox/19.0"); | |
var response = await client.SendAsync(request); | |
var taskData = response.Content.ReadAsStringAsync(); | |
return await taskData; | |
} | |
catch (HttpRequestException) | |
{ | |
return String.Empty; | |
} | |
} | |
} | |
} | |
} |
Then I added code to my POST method to perform the GET
[HttpPost] | |
[Route("/PushEndPoint")] | |
public async string PushEndPoint(string payload) | |
{ | |
try | |
{ | |
var pushDetails = JsonConvert.DeserializeObject<GitHubPushJson>(payload); | |
var commitsString = await GitHubGetHelper.PerformGetAsync(pushDetails.CommitsApiUrl()); | |
} | |
catch (Exception e) | |
{ | |
return "fail"; | |
} | |
return "parsed Ok"; | |
} |
This didn’t work, as GitHub kept refusing my connection due to a Protocol Violation. But it worked when I performed the GET from Postman and in my browser.
It turned out
thanks to this Stack Overflow answer
that the answer was to include a user agent. It didn’t matter what the user agent was, as long as one was included. So I chose to pretend that the WebApi was Firefox
Because why not
It was a simple change:
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net.Http; | |
using System.Threading.Tasks; | |
namespace asti.GitHubHookApi.Helpers | |
{ | |
public static class GitHubGetHelper | |
{ | |
public static async Task<string> PerformGetAsync(string urlToResource) | |
{ | |
var fullUrlWithAuth = GitHubApiUrlHelper.AddOathToApiCall(urlToResource); | |
using (var client = new HttpClient()) | |
{ | |
try | |
{ | |
var request = new HttpRequestMessage() | |
{ | |
RequestUri = fullUrlWithAuth, | |
Method = HttpMethod.Get | |
}; | |
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", | |
"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:19.0) Gecko/20100101 Firefox/19.0"); | |
var response = await client.SendAsync(request); | |
var taskData = response.Content.ReadAsStringAsync(); | |
return await taskData; | |
} | |
catch (HttpRequestException) | |
{ | |
return String.Empty; | |
} | |
} | |
} | |
} | |
} |
The GET started working after that.
Committing to the Commit
Now that I had the commit JSON
and the documentation
I could write a class which would have the file paths for all of the files contained within the pushed commit.
namespace asti.GitHubHookApi.Models | |
{ | |
public class GitHubCommitJson | |
{ | |
[JsonProperty("files")] | |
public ICollection<GitHubCommitFileDetails> Files { get; set; } | |
[JsonProperty("commit")] | |
public GitHubCommitDetails CommitDetails { get; set; } | |
} | |
public class GitHubCommitDetails | |
{ | |
[JsonProperty("author")] | |
public GitHubAuthorDetails Author { get; set; } | |
} | |
public class GitHubAuthorDetails | |
{ | |
[JsonProperty("name")] | |
public string Name { get; set; } | |
[JsonProperty("email")] | |
public string Email { get; set; } | |
} | |
public class GitHubCommitFileDetails | |
{ | |
[JsonProperty("filename")] | |
public string Filename { get; set; } | |
[JsonProperty("modified")] | |
// TODO make this into an enum | |
public string Status { get; set; } | |
[JsonProperty("raw_url")] | |
public string RawUrl { get; set; } | |
[JsonProperty("additions")] | |
public int Additions { get; set; } | |
[JsonProperty("deletions")] | |
public int Deletions { get; set; } | |
public int NumberOfModifications() | |
{ | |
return Additions + Deletions; | |
} | |
} | |
} |
As with the original JSON data from the push, I decided to only parse the most important data
otherwise I’d have been there all night, creating a class hierarchy
The initial version of our application would deal with javascript files only. We had plans to extend this, but for now it would do.
we only had 24 hours, you know
So I needed to figure out which files where javascript and send their file paths (along with the number of changes in the file and the author of the commit) to Zac’s Node endpoint.
I decided to use a Singleton to do the actual POST to Zac’s endpoint
Because why not?
and a helper which the Singleton would use to perform the POST.
using asti.GitHubHookApi.Models; | |
using Newtonsoft.Json; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Threading.Tasks; | |
namespace asti.GitHubHookApi.Helpers | |
{ | |
public sealed class LinterSingleton | |
{ | |
private static readonly LinterSingleton instance = new LinterSingleton(); | |
static LinterSingleton() | |
{ | |
} | |
private LinterSingleton() | |
{ | |
} | |
public static LinterSingleton Instance | |
{ | |
get | |
{ | |
return instance; | |
} | |
} | |
public string SendFilesToLinter(List<LintData> fileData) | |
{ | |
var json = JsonConvert.SerializeObject(fileData); | |
var response = LinterHelper.PerformPostAsyc(json); | |
return response.Result; | |
} | |
} | |
} |
This Singleton isn’t thread safe. See: my comment about 24 hours
using asti.GitHubHookApi.Models; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net.Http; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace asti.GitHubHookApi.Helpers | |
{ | |
public class LinterHelper | |
{ | |
private static string LinterPostUrl = "list-post-url"; | |
public static async Task<string> PerformPostAsyc(string data) | |
{ | |
using (var client = new HttpClient()) | |
{ | |
var responce = await client.PostAsync(LinterPostUrl, | |
new StringContent(data, Encoding.UTF8, "application/json")); | |
var responseData = await responce.Content.ReadAsStringAsync(); | |
return responseData; | |
} | |
} | |
} | |
} |
and I consumed these in the new version of the PushEndPoint like this:
[HttpPost] | |
[Route("/PushEndPoint")] | |
public async void PushEndPoint(string payload) | |
{ | |
var pushDetails = JsonConvert.DeserializeObject<GitHubPushJson>(payload); | |
var commitsString = await GitHubGetHelper.PerformGetAsync(pushDetails.CommitsApiUrl()); | |
var commits = JsonConvert.DeserializeObject<GitHubCommitJson>(commitsString); | |
var jsFileUrls = commits.Files | |
.Where(fi => fi.Filename.EndsWith(".js")) | |
.Select(fi => new LintData | |
{ | |
FileUrl = fi.RawUrl, | |
ModCount = fi.NumberOfModifications(), | |
UserName = commits.CommitDetails.Author.Email | |
}).ToList(); | |
LinterSingleton.Instance.SendFilesToLinter(jsFileUrls); | |
} |
A quick check with Zac showed that the file paths were coming through OK.
What Now?
I started looking into both Trello and Google Docs APIs.
I’m not going to lie, I didn’t get very far with them. Then again, it was 3 or 4 am by this point and I’d been working solidly since midday.
Not to mention an early start, after a late night
We took the group decision that, since I wasn’t getting anywhere with them, I should take a look at building an MVC app which would represent a call centre timer.
Short Brief
A start button, a stop button, a timer, and post the data to the server. Done
Can’t really get more succinct than that, right?
I created a .NET MVC application
NOT a .NET Core one – which was amazing foresight
and threw this code into the Home view
<div class="jumbotron"> | |
<h1>Customer Service Logger</h1> | |
<div class="row"> | |
<div class="col-xs-6"> | |
<input type="text" id="username" placeholder="username" /> | |
</div> | |
<div class="col-xs-6"> | |
<a id="btn-signin" class="btn btn-success btn-lg">Sign in</a> | |
</div> | |
</div> | |
<p class="lead">Log your customer service interaction by clicking "Start" when you begin your conversation with a customer</p> | |
<p class="lead">Once the customer is satisfied with the service you have suplied, click "Stop"</p> | |
<div class="pull-left"><a id="btn-start" class="btn btn-success btn-lg">Start</a></div> | |
<div class="pull-left"><a id="btn-stop" class="btn btn-success btn-lg" style="display:none;">Stop</a></div> | |
<div id="clockdiv" style="display:none;"> | |
<div> | |
<span class="minutes"></span> | |
<div class="smalltext">Minutes</div> | |
</div> | |
<div> | |
<span class="seconds"></span> | |
<div class="smalltext">Seconds</div> | |
</div> | |
</div> | |
</div> | |
@section scripts{ | |
<script type="text/javascript"> | |
var running = false; | |
var timeInterval = null; | |
function getTimeRemaining(endtime) { | |
var t = Date.parse(endtime) - Date.parse(new Date()); | |
var seconds = Math.floor((t / 1000) % 60); | |
var minutes = Math.floor((t / 1000 / 60) % 60); | |
return { | |
'minutes': minutes, | |
'seconds': seconds | |
}; | |
} | |
function initializeClock(id, endtime) { | |
var clock = document.getElementById(id); | |
var minutesSpan = clock.querySelector('.minutes'); | |
var secondsSpan = clock.querySelector('.seconds'); | |
function updateClock() { | |
if (running) { | |
var t = getTimeRemaining(endtime); | |
minutesSpan.innerHTML = ('0' + t.minutes).slice(-2); | |
secondsSpan.innerHTML = ('0' + t.seconds).slice(-2); | |
if (t.total <= 0) { | |
clearInterval(timeinterval); | |
} | |
} | |
} | |
updateClock(); | |
var timeinterval = setInterval(updateClock, 1000); | |
} | |
$(document).ready(function () { | |
$('#btn-signin').on('click', function () { | |
$('#username').attr('disabled', 'disabled'); | |
$('#btn-signin').hide(); | |
}); | |
$('#btn-start').on('click', function () { | |
if ($('#username').val().length === 0) { | |
alert('Please sign in'); | |
} else { | |
$('#btn-start').hide(); | |
$('#btn-stop').show(); | |
$('#clockdiv').show(); | |
running = true; | |
var deadline = new Date(Date.parse(new Date()) + 2 * 60 * 1000); | |
initializeClock('clockdiv', deadline); | |
} | |
}); | |
$('#btn-stop').on('click', function () { | |
$('#btn-start').hide(); | |
$('#btn-stop').hide(); | |
running = false; | |
var timeRemaining = ($('.minutes').text() * 60) + $('.seconds').text(); | |
var route = 'game-server-url'; | |
var model = { | |
username: $('#username').val(), | |
completed: timeRemaining > 0, | |
timeRemaining: timeRemaining | |
} | |
$.post(route, model, function () { | |
}).fail(function () { | |
}).always(function () { | |
$('#btn-start').show(); | |
$('#btn-stop').hide(); | |
var deadline = new Date(Date.parse(new Date()) + 15 * 60 * 1000); | |
initializeClock('clockdiv', deadline); | |
}); | |
}); | |
}); | |
</script> | |
} |
A call centre operative supplies their name, clicks “Start” when they pick up the phone, and clicks “Stop” when they hang up. The call data is JSON’d then posted directly to the game server.
Simple.
Tidying Up, UI and Video
By this point, it was time for breakfast. Only 5 hours to go and we still had a lot to do.
We needed:
- A UI dashboard (showing all stats for all users)
- A user profile page (showing the stats for a single user)
- Various tidying up jobs
- A video to actually submit our app
After a chat with James about what I needed to do to get the user data, I created the classes and threw together an MVC app for the dashboard (adding it to the MVC app that I’d created for the Call Centre application).
all the while an exhaustion based case of impostor syndrome started to set in
All that was left was to implement the styling created by James and Paul. James did the design on our whiteboard and Paul threw it together in Bootstrap studio in minutes.
Then it was down to me to apply the styling.
Once that as done, it was testing time.
During which Zac created our submission video, which you can watch here:
Submitting, Chilling, and Results
Once Zac had put the video together
and it really is ace!
with parts from each of us, we submitted the video and chilled the eff out.
there was Rocket League on a PS4 and extremely comfy chairs. I chose the chairs and nearly fell asleep
The next thing we knew it was results time.
The short version is that we didn’t win.
We’d overshot the mark, by going from 0 to a full line of business app in 24 hours. The other submissions (for the challenge we had entered) were great and targeted the problem better than we had, because we’d wandered a little from the scope a little.
Scope and feature creep are real things.
Reflections
The amount of work that we’d put in was phenomenal.
even with me complaining that I hadn’t done enough; stupid Impostor Syndrome
I remain convinced that if this application was built properly, without cutting any corners, and in a fully Agile environment, it would have taken us a week to get to where we were on Sunday at 12 midday.
And we did it in 24 hours.
That’s an amazing achievement.