Skip to content

Instantly share code, notes, and snippets.

@park-brian
Last active September 10, 2019 19:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save park-brian/019963d28a3985217b610b23c162ce15 to your computer and use it in GitHub Desktop.
Save park-brian/019963d28a3985217b610b23c162ce15 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Hacker News</title>
<style>
body {
background-color: white;
max-width: 50rem;
margin: 0 auto 10px;
font-size: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
nav {
background-color: inherit;
border-bottom: 1px solid #ddd;
position: sticky;
top: 0;
z-index: 1;
}
li {
margin-bottom: 1.5rem;
}
button {
background-color: white;
border: 1px solid #888;
cursor: pointer;
}
.pad-1 {
padding: 1rem;
}
.strong {
font-weight: bold;
}
.right {
float: right;
}
.clear::after {
content: "";
display: table-cell;
clear: both;
}
.comment {
display: block;
margin: 2rem 0 0 1rem;
}
.comment .content {
margin-left: 0.25rem;
padding-left: 1rem;
border-left: 1px solid #ddd;
line-height: 1.5;
}
</style>
</head>
<body>
<nav class="pad-1">
<a href="#news" class="strong">Hacker News</a> |
<a href="#newest">newest</a> | <a href="#ask">ask</a> |
<a href="#show">show</a> |
<a href="#jobs">jobs</a>
<div class="clear right">
<button onclick="previous()">&lt;</button>
<button onclick="next()">&gt;</button>
</div>
</nav>
<main id="root"></main>
<script>
var root = document.querySelector("#root");
// shortcuts to create commonly used html elements
["a", "div", "span", "ol", "li", "details", "summary"].forEach(function(
tag
) {
window[tag] = h.bind(h, tag);
});
/** Re-render when the url hash changes */
onhashchange = render;
render();
function render() {
getHN(getState(), function(response) {
while (root.firstChild) {
root.removeChild(root.firstChild);
}
root.appendChild(
Array.isArray(response)
? renderFeed(response)
: renderItem(response)
);
scrollTo(0, 0);
});
}
/**
* Renders a feed, which is an ordered list
* of newsfeed item summaries
*
* @param items {object[]} - An array of HN items
* @return {HTMLElement} - The item list
*/
function renderFeed(items) {
// HN api returns 30 items per page
var offset = 1 + (getState()[1] - 1) * 30;
return ol(
{ start: offset },
items.map(function(item) {
return li(null, renderItemSummary(item));
})
);
}
/**
* Renders an item summary, which contains
* links to the article and comments page,
* as well as other information about the item
*
* @param item {object} - A HN new item
* @return {HTMLElement} - The item summary
*/
function renderItemSummary(item) {
var isInternal = !item.domain;
var commentsUrl = "#item/" + item.id;
var itemUrl = isInternal ? commentsUrl : item.url;
return div(null, [
a({ href: itemUrl, className: "strong" }, item.title),
h("br"),
item.points && pluralCount(item.points, "point") + " by ",
item.user,
" " + item.time_ago + " | ",
a({ href: commentsUrl }, pluralCount(item.comments_count, "comment"))
]);
}
/**
* Renders a single HN item, which consists of the
* item summary and comments
*
* @param item {object} A HN news item which contains
* a tree of comments
* @return {HTMLElement} - The item summary + comments
*/
function renderItem(item) {
var comments = item.comments.map(renderComment);
return div(
{ className: "pad-1" },
[renderItemSummary(item)].concat(comments)
);
}
/**
* Renders a comment, including its children
*
* @param comment {object} A HN comment
* @return {HTMLElement} - A recursively generated comment tree
*/
function renderComment(comment) {
return details(
{ className: "comment", open: "open" },
[
summary(null, [h("b", null, comment.user), " " + comment.time_ago]),
div({ className: "content" }, [div({ innerHTML: comment.content })])
].concat(comment.comments.map(renderComment))
);
}
/**
* Gets a json resource given a url
*
* @param url {string} - The resource to retrieve
* @param callback {function} - A callback for the json response
*/
function getJSON(url, callback) {
var request = new XMLHttpRequest();
request.onload = function() {
callback(JSON.parse(request.responseText));
};
request.open("GET", url);
request.send();
}
/**
* Gets a resource from the Hacker News PWA API
* @param resource {['news'|'newest'|'ask'|'show'|'jobs'|'item'|'user', string]} -
* The resource to retrieve, given as [type, id]
* @param callback {function} - A callback for the retrieved resource
* @example getHN(['news', 1], handleResponse)
*/
function getHN(resource, callback) {
var url = "https://api.hnpwa.com/v0/" + resource.join("/") + ".json";
getJSON(url, callback);
}
/**
* Gets the current application state from the location hash
* This is always an array containing two elements: [resource type, id]
* @return {[string, string]}
*/
function getState() {
var state = location.hash
.substr(1)
.split("/")
.filter(Boolean);
state[0] = state[0] || "news"; // default resource type
state[1] = state[1] || "1"; // default id
return state;
}
/**
* Moves to the next page (resource id)
*/
function next() {
var state = getState();
++state[1];
if (isValidState(state)) location.hash = state.join("/");
}
/**
* Moves to the previous page (resource id)
*/
function previous() {
var state = getState();
--state[1];
if (isValidState(state)) location.hash = state.join("/");
}
/**
* Returns true if the provided state (resource/id) is valid.
* @param state {[string, string]}
*/
function isValidState(state) {
var resource = state[0];
var id = state[1];
// maxId will be undefined for invalid resources
var maxId = {
news: 10,
newest: 7,
show: 2,
ask: 2,
jobs: 1
}[resource];
return maxId !== undefined && id <= maxId && id > 0;
}
/**
* Pluralizes a noun, including the quantity
* in the pluralized string
* @param quantity {number} - The quantity of the noun
* @param singular {string} - The singular form of the noun
* @param plural {string} The plural form of the noun
*/
function pluralCount(quantity, singular, plural) {
return quantity + " " +
(quantity == 1
? singular
: plural || singular + "s");
}
/**
* Creates an html element
* @param tag {string} - The element's tag name
* @param props {object} - The element's properties
* @param children {(Node|string|number)[]} - The element's children
*/
function h(tag, props, children) {
var e = document.createElement(tag);
if (!Array.isArray(children)) children = [children];
for (var key in props || {}) {
e[key] = props[key];
}
(children || []).filter(Boolean).forEach(function(child) {
if (["string", "number"].indexOf(typeof child) > -1)
child = document.createTextNode(child);
e.appendChild(child);
});
return e;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment