Skip to content

Commit fe849d2

Browse files
Implement solution.
1 parent fdbd96b commit fe849d2

16 files changed

+6970
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
build

package-lock.json

Lines changed: 6379 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "hn-typescript",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"dev": "webpack-dev-server",
8+
"start": "webpack-dev-server",
9+
"build": "webpack"
10+
},
11+
"keywords": [],
12+
"author": "Alex Anderson",
13+
"license": "ISC",
14+
"devDependencies": {
15+
"css-loader": "^4.3.0",
16+
"html-webpack-plugin": "^4.5.0",
17+
"style-loader": "^1.2.1",
18+
"ts-loader": "^8.0.4",
19+
"typescript": "^4.0.3",
20+
"webpack": "^4.44.2",
21+
"webpack-cli": "^3.3.12",
22+
"webpack-dev-server": "^3.11.0"
23+
}
24+
}

src/api.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const TOP_POSTS_URL = "https://hacker-news.firebaseio.com/v0/topstories.json";
2+
const NEW_POSTS_URL = "https://hacker-news.firebaseio.com/v0/newstories.json";
3+
const USER_URL_BASE = "https://hacker-news.firebaseio.com/v0/user/";
4+
const STORY_URL_BASE = "https://hacker-news.firebaseio.com/v0/item/";
5+
6+
function getStoryUrl(story: number) {
7+
return `${STORY_URL_BASE}${story}.json`;
8+
}
9+
function getUserUrl(username: string) {
10+
return `${USER_URL_BASE}${username}.json`;
11+
}
12+
13+
export interface Story {
14+
by: string;
15+
descendants: number;
16+
id: number;
17+
kids: number[];
18+
score: number;
19+
time: number;
20+
title: string;
21+
text: string;
22+
type: "story" | "comment" | "job" | "poll";
23+
url: string;
24+
}
25+
26+
export interface User {
27+
about: string;
28+
created: number;
29+
delay: number;
30+
id: string;
31+
karma: number;
32+
submitted: number[];
33+
}
34+
35+
let itemCache: { [itemId: number]: Story } = {};
36+
37+
export async function getItem(id: number): Promise<Story> {
38+
if (itemCache[id]) return itemCache[id];
39+
const item = await (await fetch(getStoryUrl(id))).json();
40+
itemCache[id] = item;
41+
return item;
42+
}
43+
44+
export async function getStories(which: "top" | "new") {
45+
const url = which === "top" ? TOP_POSTS_URL : NEW_POSTS_URL;
46+
47+
const postIDs: number[] = await (await fetch(url)).json();
48+
49+
return Promise.all(postIDs.slice(0, 30).map(getItem));
50+
}
51+
52+
export async function getComments(storyId: number) {
53+
const story = await getItem(storyId);
54+
55+
const comments = story.kids.slice(0, 30).map((id) => getItem);
56+
return Promise.all(comments);
57+
}
58+
59+
let userCache: { [username: string]: User } = {};
60+
61+
export async function getUser(username: string) {
62+
if (userCache[username]) return userCache[username];
63+
const user: User = await (await fetch(getUserUrl(username))).json();
64+
userCache[username] = user;
65+
return user;
66+
}

src/content.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { getStories, getUser, getItem, Story, User } from "./api";
2+
import { createStory } from "./createStory";
3+
import { renderUser } from "./renderUser";
4+
import { renderLoader } from "./renderLoader";
5+
import { renderComments } from "./renderComments";
6+
7+
// This replaces browser routes
8+
let contentState: "top" | "new" | "user" | "comments" = "top";
9+
let username: string;
10+
let storyId: number;
11+
12+
async function renderPage(mainContent: HTMLElement) {
13+
mainContent.innerHTML = "";
14+
mainContent.appendChild(renderLoader());
15+
16+
if (contentState === "top" || contentState === "new") {
17+
const stories = await renderStories(contentState);
18+
mainContent.innerHTML = "";
19+
const postsContainer = document.createElement("ul");
20+
mainContent.appendChild(postsContainer);
21+
stories.forEach((story) => {
22+
if (story) {
23+
postsContainer.appendChild(story);
24+
}
25+
});
26+
}
27+
28+
if (contentState === "user") {
29+
mainContent.innerHTML = "";
30+
mainContent.appendChild(await renderUser(username));
31+
}
32+
if (contentState === "comments") {
33+
mainContent.innerHTML = "";
34+
mainContent.appendChild(await renderComments(storyId));
35+
}
36+
}
37+
38+
async function renderStories(which: "top" | "new") {
39+
const posts = await getStories(which);
40+
const stories = posts.map(createStory);
41+
return stories;
42+
}
43+
export function renderContent(mainContent: HTMLElement) {
44+
renderPage(mainContent);
45+
46+
document.addEventListener("topLink", () => {
47+
contentState = "top";
48+
renderPage(mainContent);
49+
});
50+
document.addEventListener("newLink", () => {
51+
contentState = "new";
52+
renderPage(mainContent);
53+
});
54+
55+
document.addEventListener("comments", (event) => {
56+
const { story } = (event as CustomEvent<{ story: number }>).detail;
57+
contentState = "comments";
58+
storyId = story;
59+
renderPage(mainContent);
60+
});
61+
document.addEventListener("user", (event) => {
62+
const { user } = (event as CustomEvent<{ user: string }>).detail;
63+
contentState = "user";
64+
username = user;
65+
renderPage(mainContent);
66+
});
67+
}

src/createStory.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Story } from "./api";
2+
import { formatDate } from "./utils/formatDate";
3+
4+
export function createStory(story: Story) {
5+
if (!story.url || !story.title) return;
6+
const storyWrapper = document.createElement("li");
7+
storyWrapper.classList.add("post");
8+
const title = document.createElement("a");
9+
title.classList.add("link");
10+
title.href = story.url;
11+
title.innerText = story.title;
12+
storyWrapper.appendChild(title);
13+
14+
const tagline = document.createElement("p");
15+
tagline.classList.add("meta-info");
16+
tagline.appendChild(document.createTextNode("by "));
17+
18+
const authorLink = document.createElement("a");
19+
authorLink.href = "#";
20+
authorLink.addEventListener("click", () => {
21+
document.dispatchEvent(
22+
new CustomEvent<{ user: string }>("user", { detail: { user: story.by } })
23+
);
24+
});
25+
authorLink.innerText = story.by;
26+
tagline.appendChild(authorLink);
27+
28+
tagline.appendChild(
29+
document.createTextNode(
30+
` on ${formatDate(new Date(story.time * 1000))} with `
31+
)
32+
);
33+
34+
const commentCount = document.createElement("a");
35+
commentCount.href = "#";
36+
commentCount.innerText = story.descendants?.toString() ?? "0";
37+
commentCount.addEventListener("click", () => {
38+
document.dispatchEvent(
39+
new CustomEvent<{ story: number }>("comments", {
40+
detail: { story: story.id },
41+
})
42+
);
43+
});
44+
tagline.appendChild(commentCount);
45+
46+
tagline.appendChild(document.createTextNode(" comments"));
47+
48+
storyWrapper.appendChild(tagline);
49+
return storyWrapper;
50+
}

src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { init } from "./init";
2+
import { renderLayout } from "./layout";
3+
import { renderContent } from "./content";
4+
import "./style.css";
5+
6+
const root = init();
7+
8+
const { layout, mainContent } = renderLayout();
9+
10+
root.appendChild(layout);
11+
12+
renderContent(mainContent);

src/init.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function init() {
2+
const div = document.createElement("div");
3+
div.id = "app";
4+
document.body.appendChild(div);
5+
return div;
6+
}

src/layout.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
let colorMode = "light";
2+
3+
export function renderLayout() {
4+
const layout = document.createElement("div");
5+
layout.id = "layout";
6+
layout.classList.add("container");
7+
8+
const nav = document.createElement("nav");
9+
nav.classList.add("row");
10+
nav.classList.add("space-between");
11+
layout.appendChild(nav);
12+
13+
const navList = document.createElement("ul");
14+
navList.classList.add("row");
15+
navList.classList.add("nav");
16+
nav.appendChild(navList);
17+
18+
const topItem = document.createElement("li");
19+
navList.appendChild(topItem);
20+
const topLink = document.createElement("a");
21+
topLink.innerText = "Top";
22+
topLink.classList.add("active");
23+
topLink.classList.add("nav-link");
24+
topLink.href = "#";
25+
topLink.addEventListener("click", () =>
26+
document.dispatchEvent(new Event("topLink"))
27+
);
28+
topItem.appendChild(topLink);
29+
30+
const newItem = document.createElement("li");
31+
navList.appendChild(newItem);
32+
const newLink = document.createElement("a");
33+
newLink.innerText = "New";
34+
newLink.classList.add("nav-link");
35+
newLink.href = "#";
36+
newLink.addEventListener("click", () =>
37+
document.dispatchEvent(new Event("newLink"))
38+
);
39+
newItem.appendChild(newLink);
40+
41+
const darkModeToggle = document.createElement("button");
42+
darkModeToggle.innerText = "🔦";
43+
darkModeToggle.classList.add("btn-clear");
44+
darkModeToggle.style.fontSize = "30px";
45+
46+
darkModeToggle.addEventListener("click", () => {
47+
if (colorMode === "light") {
48+
colorMode = "dark";
49+
document.body.classList.add("dark");
50+
darkModeToggle.innerText = "💡";
51+
} else {
52+
colorMode = "light";
53+
document.body.classList.remove("dark");
54+
darkModeToggle.innerText = "🔦";
55+
}
56+
});
57+
58+
nav.appendChild(darkModeToggle);
59+
60+
const mainContent = document.createElement("main");
61+
layout.appendChild(mainContent);
62+
63+
// Add event listeners for updating our styles
64+
document.addEventListener("topLink", () => {
65+
newLink.classList.remove("active");
66+
topLink.classList.add("active");
67+
});
68+
document.addEventListener("newLink", () => {
69+
newLink.classList.add("active");
70+
topLink.classList.remove("active");
71+
});
72+
73+
return { layout, mainContent };
74+
}

src/renderComments.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { getItem } from "./api";
2+
import { createStory } from "./createStory";
3+
import { formatDate } from "./utils/formatDate";
4+
5+
export async function renderComments(storyId: number) {
6+
const story = await getItem(storyId);
7+
const commentsContainer = document.createElement("div");
8+
commentsContainer.classList.add("comments-container");
9+
10+
const storyMarkup = createStory(story);
11+
12+
if (storyMarkup) {
13+
commentsContainer.appendChild(storyMarkup);
14+
}
15+
16+
const comments = await Promise.all(
17+
story.kids.slice(0, 30).map(renderComment)
18+
);
19+
20+
comments.forEach((comment) => {
21+
commentsContainer.appendChild(comment);
22+
});
23+
return commentsContainer;
24+
}
25+
26+
async function renderComment(id: number) {
27+
const story = await getItem(id);
28+
const commentContainer = document.createElement("div");
29+
commentContainer.classList.add("comment");
30+
31+
const commentTagline = document.createElement("p");
32+
commentTagline.classList.add("meta-info");
33+
commentTagline.appendChild(document.createTextNode("by "));
34+
35+
const authorLink = document.createElement("a");
36+
authorLink.href = "#";
37+
authorLink.addEventListener("click", () => {
38+
document.dispatchEvent(
39+
new CustomEvent<{ user: string }>("user", { detail: { user: story.by } })
40+
);
41+
});
42+
authorLink.innerText = story.by;
43+
commentTagline.appendChild(authorLink);
44+
45+
commentTagline.appendChild(
46+
document.createTextNode(` on ${formatDate(new Date(story.time * 1000))}`)
47+
);
48+
commentContainer.appendChild(commentTagline);
49+
50+
const comment = document.createElement("div");
51+
comment.classList.add("comment-text");
52+
comment.innerHTML = story.text;
53+
commentContainer.appendChild(comment);
54+
55+
return commentContainer;
56+
}

0 commit comments

Comments
 (0)