Skip to content

Instantly share code, notes, and snippets.

@KrofDrakula
Last active May 12, 2016 20:05
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save KrofDrakula/075727b55044c20cb3926c1beae419cc to your computer and use it in GitHub Desktop.
scrolling: yes
height: 800
(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);
<!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 &quot;pixeltrix&quot;" 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