Skip to content

Instantly share code, notes, and snippets.

@tophtucker
Last active June 1, 2016 17:26
Show Gist options
  • Save tophtucker/fc10549dcb5cbb351b77470b929d975e to your computer and use it in GitHub Desktop.
Save tophtucker/fc10549dcb5cbb351b77470b929d975e to your computer and use it in GitHub Desktop.
Blocks by people you follow
scrolling: yes
height: 1000
border: no

View a stream of most recently-updated blocks from the users you follow (up to 100, currently) on Github. Change the username and hit enter. If Github API fails, goes to a random little handful of decent people.

Issues:

  • Sometimes runs into the rate limit for unauthenticated GitHub API requests
  • Runs into Blocks API CORS issue if running locally
  • Doesn't page through your Github follows, if you follow >100 people
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: "Helvetica Neue", Helvetica, sans-serif;
width: 970px;
margin: 0 auto;
}
h1 {
text-rendering: optimizeLegibility;
font-size: 46px;
font-weight: 500;
letter-spacing: -1px;
margin: 0.1em 0;
padding: 1.5rem 0;
color: #999;
}
h1 input.loading {
animation: flashing 1s ease infinite;
}
h1 input.error {
color: red;
}
.navigation {
float: right;
color: #999;
padding: 3.4rem 10px 0 0;
}
.navigation span:not(.active):hover {
cursor: pointer;
text-decoration: underline;
}
.navigation span.active {
color: black;
}
a {
color: black;
}
a:not(:hover) {
text-decoration: none;
}
.username {
color: black;
text-rendering: inherit;
font-size: inherit;
font-weight: inherit;
font-family: inherit;
letter-spacing: inherit;
border: none;
border-bottom: 1px dashed #999;
width: 7em;
padding: 0;
}
.gist-outer {
margin: 0 10px 10px 0;
width: 232px;
display: inline-block;
float: left;
}
.gist-outer .timestamp {
color: #999;
font-size: 11px;
margin-bottom: 3px;
}
.gists .gist {
background-position: 50% 0%;
border: solid 1px #eee;
border-bottom: solid 1px #ccc;
-webkit-box-sizing: border-box;
-ms-box-sizing: border-box;
-moz-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
display: inline-block;
line-height: normal;
position: relative;
text-shadow: 1px 1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, -1px -1px 0 #fff;
-webkit-transition: ease 750ms background-position;
-moz-transition: ease 750ms background-position;
-ms-transition: ease 750ms background-position;
-o-transition: ease 750ms background-position;
transition: ease 750ms background-position;
width: 232px;
padding: 8px 10px;
}
.gists .gist--thumbnail {
height: 82px;
color: #000;
overflow: hidden;
}
.gists .gist:hover,
.gists .gist:focus {
background-color: #eee;
background-position: 50% 100%;
text-decoration: none;
}
.gists .gist-description {
overflow: hidden;
text-overflow: ellipsis;
max-height: 55px;
}
.gists .gist-author {
color: #777;
font-weight: 300;
}
.gists .gist:hover .gist-underline {
text-decoration: underline;
}
@keyframes flashing {
50% { opacity: .4; }
}
</style>
<body>
<header>
<div class="navigation">
<span data-sort="updated_at">Updated</span> / <span data-sort="created_at">Created</span>
</div>
<h1>@<input type="text" class="username">’s timeline</h1>
</header>
<div class="gists"></div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/3.2.2/es6-promise.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
<script src="//d3js.org/d3.v4.0.0-alpha.35.min.js" charset="utf-8"></script>
<script>
var defaultFollows = [
{'login': '1wheel'},
{'login': 'armollica'},
{'login': 'atmccann'},
{'login': 'blacki'},
{'login': 'dwtkns'},
{'login': 'enjalot'},
{'login': 'gka'},
{'login': 'jasondavies'},
{'login': 'kennelliott'},
{'login': 'mbostock'},
{'login': 'monfera'},
{'login': 'robinhouston'},
{'login': 'tophtucker'},
{'login': 'veltman'}
];
var formatTime = d3.timeFormat("%B %d, %Y");
var input = d3.select('.username')
.on('keypress', handleKeypress)
.on('focus', handleFocus)
.on('blur', handleBlur);
if(window.location.hash && window.location.hash.split('#')[1]) {
var user = window.location.hash.split('#')[1];
input.node().value = user;
fetchFollows(user);
} else {
input.node().value = 'tophtucker';
fetchBlocks(defaultFollows);
}
function fetchFollows(user) {
console.log('Fetching users followed by', user);
input.classed('loading', true);
input.node().value = user;
window.location.hash = user;
fetch('https://api.github.com/users/' + user + '/following?per_page=100').then(function(response) {
if(response.ok) {
return response.text();
} else {
input
.classed('loading', false)
.classed('error', true);
return JSON.stringify(defaultFollows);
}
}).then(function(text) {
return JSON.parse(text);
}).then(fetchBlocks);
}
function fetchBlocks(follows) {
input.classed('loading', true);
Promise.all(follows.map(function(user) {
console.log('Fetching', user.login, '’s blocks');
return fetch('http://bl.ocks.org/' + user.login + '/1.json').then(function(response) {
return response.ok ? response.text() : Promise.reject(response.status);
}).then(function(text) {
return JSON.parse(text).map(function(d) { d.user = user; return d; });
});
})).then(function(blocksByUser) {
input.classed('loading', false);
var blocks = [].concat.apply([], blocksByUser);
return blocks;
}, function(rejection) {
input
.classed('loading', false)
.classed('error', true);
}).then(renderBlocks);
}
function renderBlocks(blocks, limit, sortKey) {
if(limit===undefined) limit = 100;
if(sortKey===undefined) sortKey = 'updated_at';
var block = d3.select('.gists').selectAll('.gist-outer')
.data(
blocks.sort(sortBy(sortKey)).slice(0,limit),
function(d) { return d.id; }
);
block.exit().remove();
blockEnter = block.enter()
.append('div')
.classed('gist-outer', true);
blockEnter.append('a')
.classed('user', true)
.attr('href', function(d) { return '/' + d.user.login; })
.text(function(d) { return d.user.login; })
blockEnter.append('div')
.classed('timestamp', true)
.attr('data-sort', 'updated_at')
.text(function(d) { return "Updated " + formatTime(new Date(d['updated_at'])); });
blockEnter.append('div')
.classed('timestamp', true)
.attr('data-sort', 'created_at')
.text(function(d) { return "Created " + formatTime(new Date(d['created_at'])); });
blockEnter.append('a')
.classed('gist', true)
.classed('gist--thumbnail', true)
.attr('href', function(d) { return '/' + d.user.login + '/' + d.id; })
.style('background-image', function(d) {
return 'url("/' + d.user.login + '/raw/' + d.id + '/thumbnail.png")';
})
.append('div')
.classed('gist-description', true)
.classed('gist-underline', true)
.text(function(d) { return d.description; });
blockEnter.merge(block)
.order()
.selectAll('.timestamp')
.style('display', function(d,i) {
return sortKey === this.dataset.sort ? 'block' : 'none';
});
d3.selectAll('.navigation span')
.classed('active', function(d) {
return sortKey === this.dataset.sort;
});
d3.selectAll('.navigation span').on('click', function() {
d3.selectAll('.navigation span').on('click', null);
renderBlocks(blocks, limit, this.dataset.sort);
});
d3.select(window).on('scroll', function() {
if(window.scrollY >= document.body.scrollHeight - window.innerHeight - 20) {
d3.select(window).on('scroll', null);
renderBlocks(blocks, limit + 100, sortKey);
}
});
}
function handleKeypress() {
if(d3.event.which === 13) {
fetchFollows(this.value);
}
}
function handleFocus() {
d3.select(this).classed('error', false);
}
function handleBlur() {
fetchFollows(this.value);
}
function sortBy(key) {
return function(a,b) {
return new Date(b[key]) - new Date(a[key]);
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment