|
<!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> |