Last active
May 12, 2016 20:05
Star
You must be signed in to star a gist
Octocatificator! http://bl.ocks.org/KrofDrakula/075727b55044c20cb3926c1beae419cc
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
scrolling: yes | |
height: 800 |
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
(function(global) { | |
// -------[ CLASS & FUNCTION DEFINITIONS ]------- | |
// A persistent storage class that uses localStorage to cache user data | |
function Storage(storageName) { | |
this.storageName = storageName; | |
this.load(); | |
} | |
Storage.prototype.load = function() { | |
this.users = JSON.parse(localStorage.getItem(this.storageName) || '[]'); | |
}; | |
Storage.prototype.save = function() { | |
localStorage.setItem(this.storageName, JSON.stringify(this.users)); | |
}; | |
Storage.prototype.createUser = function(name) { | |
return { name: sanitize(name), data: null }; | |
}; | |
Storage.prototype.findUser = function(name) { | |
name = sanitize(name); | |
var matches = this.users.filter(function(user) { | |
return user.name == name; | |
}); | |
return matches.length > 0 ? matches[0] : null; | |
}; | |
Storage.prototype.saveUser = function(username, data) { | |
username = sanitize(username); | |
var user = this.findUser(username); | |
if (!user) this.users.push(user = this.createUser(username)); | |
user.data = data; | |
this.save(); | |
return user; | |
}; | |
function trim(str) { return str.toString().replace(/^\s+|\s+$/g, ''); } | |
function sanitize(name) { return trim(name).replace(/[^a-z0-9_-]/gi, '').toLowerCase(); } | |
function validateUsername(name) { return /^[a-z0-9_-]+$/i.test(name) && !/--+/g.test(name); } | |
// Use `<% ... %>` to execute blocks of JavaScript, `<%= ... %>` to write | |
// out the result of the embedded expression. | |
function tmpl(str, params) { | |
if (!str) return ''; | |
function generateOutput(str) { | |
return " p.push('" + str.replace(/'/g, "\\'").split(/\r?\n/g).join("\\n');\n p.push('") + "');\n"; | |
} | |
var fn; | |
if (str.indexOf('<%') == -1) { | |
fn = function() { return str; }; | |
} else { | |
var fragments = str.split(/<%\s*|\s*%>/g), | |
body = 'var p = []; with(o) {\n', | |
insideExpression = false; | |
fragments.forEach(function(frag) { | |
if (insideExpression) { | |
if (frag[0] == '=') { | |
// it's an expression, so output its value | |
body += " p.push(" + frag.replace(/^=\s*|\s*$/g, '') + ");\n"; | |
} else { | |
// it's a JavaScript statement | |
body += " " + frag + '\n'; | |
} | |
} else if (frag) { | |
// literal string, just escape | |
body += generateOutput(frag); | |
} | |
insideExpression = !insideExpression; | |
}); | |
body += '} return p.join("");'; | |
try { | |
fn = new Function('o', body); | |
} catch(ex) { | |
var err = new Error('Cannot parse template! (see `template` property)'); | |
err.template = body; | |
throw err; | |
} | |
} | |
return params ? fn(params) : fn; | |
} | |
// main application class, instantiated below | |
function Application(root, storage) { | |
this.root = root; | |
this.storage = storage; | |
this.currentUser = null; | |
// we extract the relevant UI blocks for reference | |
this.ui = { | |
userSelector : this.root.querySelector('.user-selection'), | |
userInput : this.root.querySelector('.user-selection input[name=user]'), | |
octocatifyButton : this.root.querySelector('.user-selection [data-action=octocatify]'), | |
bustCacheCheckbox : this.root.querySelector('.user-selection input[name=force-query]'), | |
loader : this.root.querySelector('.loader'), | |
results : this.root.querySelector('.results'), | |
table : this.root.querySelector('.results .data-table'), | |
verdict : this.root.querySelector('.results .verdict'), | |
avatar : this.root.querySelector('.results .avatar img'), | |
profileLink : this.root.querySelector('.results .avatar a') | |
}; | |
this._bindFunctions(this.constructor.boundFunctions); | |
} | |
Application.boundFunctions = ['_handle*']; | |
Application.urls = { | |
repos: tmpl('https://api.github.com/users/<%= username %>/repos'), | |
}; | |
Application.templates = { | |
table: tmpl([ | |
'<table>', | |
' <thead>', | |
' <tr>', | |
' <th>Language</th>', | |
' <th>Count</th>', | |
' </tr>', | |
' </thead>', | |
' <tbody>', | |
' <% languages.forEach(function(lang) { %>', | |
' <tr>', | |
' <td><%= lang.name %></td>', | |
' <td><%= lang.count %></td>', | |
' </tr>', | |
' <% }) %>', | |
' </tbody>', | |
'</table>' | |
].join('\n')) | |
}; | |
// binds functions for scoping event handlers and such | |
Application.prototype._bindFunctions = function(list) { | |
list.forEach(function(fn) { | |
if (fn.indexOf('*') > -1) { | |
var tester = new RegExp('^' + fn.replace(/\*/g, '.*') + '$'); | |
for (var name in this) | |
if (typeof this[name] == 'function' && tester.test(name)) | |
this[name] = this[name].bind(this); | |
} else { | |
this[fn] = this[fn].bind(this); | |
} | |
}, this); | |
}; | |
Application.prototype.init = function() { | |
this.ui.userInput.addEventListener('input', this._handleUserTextboxInput); | |
this.ui.userInput.addEventListener('keypress', this._handleUserTextboxKeypress); | |
this.ui.octocatifyButton.addEventListener('click', this._handleOctocatifyButtonClick); | |
}; | |
Application.prototype._handleUserTextboxInput = function(ev) { | |
var value = trim(this.ui.userInput.value); | |
if (!value || validateUsername(value)) | |
this.ui.userInput.classList.remove('error'); | |
else | |
this.ui.userInput.classList.add('error'); | |
}; | |
Application.prototype._handleUserTextboxKeypress = function(ev) { | |
var username = trim(this.ui.userInput.value); | |
if (ev.keyCode == '13' && validateUsername(username)) { | |
ev.preventDefault(); | |
this.load(sanitize(username)); | |
} | |
}; | |
Application.prototype._handleOctocatifyButtonClick = function(ev) { | |
var username = trim(this.ui.userInput.value); | |
if (!username || !validateUsername(username)) { | |
this.ui.userInput.classList.add('error'); | |
return; | |
} | |
this.load(sanitize(username)); | |
}; | |
// loads a given username from storage; if that fails, queries the GitHub API | |
Application.prototype.load = function(username) { | |
var user = this.storage.findUser(username); | |
// if we don't have the user's data available or if the checkbox is checked, we fetch | |
if (!user || this.ui.bustCacheCheckbox.checked) { | |
// hide previous results and user selector, show the loader | |
this.hide(this.ui.userSelector, this.ui.results); | |
this.show(this.ui.loader); | |
// we must fetch the user's repos from the GitHub API, this constructs the URL | |
var url = this.constructor.urls.repos({ username: username }); | |
// execute the fetch | |
fetch(url).then(function(response) { | |
// extract the JSON part | |
return response.json(); | |
}).then(function(data) { | |
// if the user isn't found, immediately show the user selection | |
if (data.message == 'Not Found') { | |
alert("I'm sorry, GitHub says the user doesn't exist! Please try again."); | |
this.hide(this.ui.loader, this.ui.results); | |
this.show(this.ui.userSelector); | |
return; | |
} | |
// save the fetched data and set the current user | |
this.currentUser = this.storage.saveUser(username, data); | |
// hide loader, show selector and results | |
this.hide(this.ui.loader); | |
this.show(this.ui.userSelector, this.ui.results); | |
this.ui.userInput.focus(); | |
this.renderResults(); | |
}.bind(this)); | |
} else { | |
// we already have the data, so just show the results | |
this.currentUser = user; | |
this.show(this.ui.results); | |
this.renderResults(); | |
} | |
}; | |
// renders out the results for the current user | |
Application.prototype.renderResults = function() { | |
// this maps languages into a hash map | |
var languages = this.currentUser.data.map(function(repo) { | |
return repo.language; | |
}).reduce(function(u, v) { | |
if (u.hasOwnProperty(v)) | |
u[v]++; | |
else | |
u[v] = 1; | |
return u; | |
}, {}); | |
// this one converts the same hashmap into an ordered array of objects | |
languages = Object.keys(languages).map(function(name) { | |
return { name: name, count: languages[name] }; | |
}).sort(function(a, b) { | |
return b.count - a.count; | |
}); | |
// we render out the sorted data | |
this.ui.table.innerHTML = this.constructor.templates.table({ languages: languages }); | |
// sum up the tally | |
var sum = languages.reduce(function(u, v) { return u + v.count; }, 0); | |
// then make an uneducated guess as to what the dominant language is | |
if (languages.length == 0 || languages[0].name == 'null') { | |
this.ui.verdict.innerHTML = 'I have no idea what language this user speaks.'; | |
} else if (languages[0].name == 'CSS' || languages[0].name == 'HTML') { | |
this.ui.verdict.innerHTML = 'Yeah... CSS and HTML don\'t really count.'; | |
} else if (languages.length == 1 || languages[0].count > sum / 2) { | |
this.ui.verdict.innerHTML = 'It seems that this user has an overwhelming preference for <strong>' + languages[0].name + '</strong>.'; | |
} else if (languages[0].count * 0.5 > languages[1].count) { | |
this.ui.verdict.innerHTML = 'This user is multilingual, but prefers <strong>' + languages[0].name + '<strong>.'; | |
} else { | |
this.ui.verdict.innerHTML = 'Your guess is as good as mine.'; | |
} | |
// lastly, set the user's avatar ... | |
this.ui.avatar.src = this.currentUser.data[0].owner.avatar_url; | |
// ... and the profile link | |
this.ui.profileLink.href = this.currentUser.data[0].owner.html_url; | |
}; | |
Application.prototype.show = function() { | |
for (var i in arguments) | |
arguments[i].style.display = 'block'; | |
}; | |
Application.prototype.hide = function() { | |
for (var i in arguments) | |
arguments[i].style.display = 'none'; | |
}; | |
// --------[ INIT ]-------- | |
const STORAGE_NAME = 'OCTOCATIFICATOR'; | |
var app = new Application(document.querySelector('body>article'), new Storage(STORAGE_NAME)); | |
app.init(); | |
})(this); |
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
<!doctype html> | |
<html> | |
<head> | |
<meta charset="utf-8"/> | |
<meta name="viewport" content="initial-scale=1"/> | |
<title>GitHub Octocatify!</title> | |
<style> | |
/* Base styling */ | |
html { padding: 0; background: linear-gradient(0deg, #bbb, #eee); color: #404040; font-size: 1.25em; min-height: 100%; } | |
td, th, input, textarea, select, button { font: inherit; } | |
h1, h2 { color: #ac5d50; } | |
h1 { font-size: 3em; } | |
h2 { font-size: 2em; } | |
strong, em { color: black; } | |
a, a:visited { color: #ac5d50; } | |
table { border-collapse: collapse; } | |
td, th { padding: 0.1em 0.3em; border: 1px groove #ddd; } | |
/* Layout */ | |
article { margin: 2em auto; max-width: 28em; } | |
section { overflow: hidden; } | |
header>h1 { position: relative; } | |
header>h1:before { position: absolute; content: " "; width: 4em; height: 4em; left: -3.5em; top: -0.1em; background: url(inspectocat.png) no-repeat transparent; background-size: contain; transform: rotate(-12deg); } | |
footer { margin-top: 3em; font-size: 0.75em; padding-top: 1em; border-top: 1px dotted #fff; font-style: italic; } | |
/* Components */ | |
input[name=user] { width: 12em; } | |
input.error { background: #fdd; } | |
button:focus { outline: none; } | |
.action { background: #91c600; color: white; border: 0.1em outset #91c600; border-radius: 0.3em; cursor: pointer; } | |
.avatar { float: right; width: 8em; padding: 0.5em 0.5em 2em 0.5em; background: white; box-shadow: 0 4px 8px rgba(0,0,0,0.5); transform: rotate(9deg); } | |
.avatar img { max-width: 100%; } | |
/* Init states */ | |
.loader, .results { display: none; } | |
</style> | |
</head> | |
<body> | |
<article> | |
<header> | |
<h1>Octocatify!</h1> | |
<p>Tired of looking through a user's profile and enumerating their repos just to see which languages they know? <strong>This is the tool you've been waiting for.</strong> Just enter a GitHub username and we'll do the rest.</p> | |
</header> | |
<section class="user-selection"> | |
<input type="text" name="user" placeholder="Something like "pixeltrix"" autofocus/> | |
<button class="action" data-action="octocatify">Octocatify!</button> | |
<label title="Use this if you think the locally stored data is stale."><input type="checkbox" name="force-query" value="1"/> Ignore cache</label> | |
</section> | |
<section class="loader"> | |
<p>Please wait while we crunch the numbers...</p> | |
<p><img src="http://www.reactiongifs.com/wp-content/uploads/2013/03/loading.gif" alt="Loader"/></p> | |
</section> | |
<section class="results"> | |
<h2>Results</h2> | |
<figure class="avatar"><a href target="_blank"><img src/></a></figure> | |
<p><strong>Et voilá!</strong> Here are the results:</p> | |
<div class="data-table"></div> | |
<p class="verdict"></p> | |
</section> | |
<footer>Lovingly crafted by <a href="http://krofdrakula.github.io/" target="_blank">KrofDrakula</a>.</footer> | |
</article> | |
<!-- A polyfill for the `fetch` API, needed by Safari --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script> | |
<!-- The actual app implementation. This is where the magic happens. --> | |
<script src="app.js"></script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment