half of the project
This commit is contained in:
parent
e9e003c55f
commit
133e9aa73e
44
index.html
44
index.html
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>CloneWars</title>
|
||||||
|
<link rel="stylesheet" href="static/bulma.css">
|
||||||
|
<link rel="stylesheet" href="static/fontawesome.css"/>
|
||||||
|
<link rel="stylesheet" href="static/solid.css"/>
|
||||||
|
<script src="static/index.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar breadcumb" role="navigation" aria-label="main navigation">
|
||||||
|
<div class="navbar-item">
|
||||||
|
<a href="index.html">
|
||||||
|
<h1 class="title">CloneWars</h1>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-item">
|
||||||
|
<p class='subtitle' id='header-sub'>The latest on <strong>Hackernews</strong>!</p>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-end">
|
||||||
|
<div class="navbar-item">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button is-info" id='notification-btn'>Notification</button>
|
||||||
|
<button class="button is-warning" id='switch-btn'>New</button>
|
||||||
|
|
||||||
|
<p class="navbar-item"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class='section' style="height: 80%">
|
||||||
|
<div class='container'>
|
||||||
|
<div id='item-box'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,394 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const pageItemBox = document.getElementById('item-box')
|
||||||
|
const headerSub = document.getElementById('header-sub')
|
||||||
|
const params = new URLSearchParams(document.location.search)
|
||||||
|
|
||||||
|
// for top listing
|
||||||
|
let items = [] // shuld be an array of items id
|
||||||
|
let currentItem = -1
|
||||||
|
|
||||||
|
// for newest listing
|
||||||
|
let newMode = params.get('order') == 'new'
|
||||||
|
let floorItem // for the later new items
|
||||||
|
let maxItem
|
||||||
|
|
||||||
|
// for the api requests
|
||||||
|
let loadedItems = 0
|
||||||
|
let loader_id
|
||||||
|
let timer = 100 // how many request pre minute
|
||||||
|
|
||||||
|
// for item query
|
||||||
|
let isQuery = params.get('id') != null
|
||||||
|
|
||||||
|
const itemType = {
|
||||||
|
job: 'job', // list
|
||||||
|
story: 'story', // list
|
||||||
|
comment: 'comment',
|
||||||
|
poll: 'poll', // list
|
||||||
|
pollopt: 'pollopt'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStories() {
|
||||||
|
const req = await fetch('https://hacker-news.firebaseio.com/v0/topstories.json')
|
||||||
|
|
||||||
|
req.json().then(resolve => {
|
||||||
|
items = resolve
|
||||||
|
currentItem = 0
|
||||||
|
|
||||||
|
loadItems()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMaxItem() {
|
||||||
|
const req = await fetch('https://hacker-news.firebaseio.com/v0/maxitem.json')
|
||||||
|
|
||||||
|
req.json().then(resolve => {
|
||||||
|
maxItem = resolve
|
||||||
|
floorItem = resolve
|
||||||
|
|
||||||
|
loadItems()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadItems() {
|
||||||
|
|
||||||
|
const shouldLoad = () => {
|
||||||
|
const maxScroll = document.documentElement.scrollHeight - document.documentElement.clientHeight
|
||||||
|
return (currentItem < items.length) && window.scrollY > maxScroll - 100 || maxScroll == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loader_id || !shouldLoad()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loader_id = setInterval(async () => {
|
||||||
|
|
||||||
|
if (!shouldLoad()) {
|
||||||
|
loader_id = clearInterval(loader_id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemId = newMode ? maxItem-- : items[currentItem++]
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item = await getItem(itemId)
|
||||||
|
makeItemListing(item)
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[loadItems] got error ${e}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
}, timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getItem(id) {
|
||||||
|
const url = `https://hacker-news.firebaseio.com/v0/item/${id}.json`
|
||||||
|
|
||||||
|
try {
|
||||||
|
return fetch(url).then(response => response.json().then(obj => obj))
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[getItem]: failed to fetch item ${id}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function makeItemListing(itemObj) {
|
||||||
|
|
||||||
|
if (itemObj === null || itemObj === undefined) {
|
||||||
|
console.log(`[makeItemListing]: null or undefined object`)
|
||||||
|
clearInterval(loader_id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemObj.deleted || itemObj.dead || itemObj.title == "[deleted]") {
|
||||||
|
console.log(`[makeItemListing]: bad item: ${itemObj.id}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemObj.type == itemType.comment || itemObj.type == itemType.pollopt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = itemObj.id
|
||||||
|
const title = itemObj.title
|
||||||
|
const url = itemObj.url
|
||||||
|
|
||||||
|
const points = itemObj.score
|
||||||
|
const author = itemObj.by
|
||||||
|
const time = itemObj.time
|
||||||
|
const date = timeSince(itemObj.time)
|
||||||
|
const comments = itemObj.descendants
|
||||||
|
|
||||||
|
const match = url && url.match(/https?:\/\/([^\/:]+)/)
|
||||||
|
const hostname = match && match[1]
|
||||||
|
|
||||||
|
let subtitle = `${points} points by ${author} ${date} ago`
|
||||||
|
|
||||||
|
if (itemObj.type != itemType.job) {
|
||||||
|
subtitle += ` | ${comments} comments`
|
||||||
|
} else {
|
||||||
|
console.log(`[makeItemListing]: job item ${title}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const element_string =`
|
||||||
|
<div class="block">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<h4 class="title is-4">${title} <a href="${url}">(${hostname})</a></h4>
|
||||||
|
<p class="subtitle is-5">${subtitle}</p>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow is-center">
|
||||||
|
<span class="icon is-small is-right" style="height: 100%">
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
const item = document.createElement('a')
|
||||||
|
item.href = (itemObj.type != itemType.job) ? "index.html?id=" + id : url
|
||||||
|
item.time = time
|
||||||
|
item.innerHTML = element_string
|
||||||
|
|
||||||
|
if (url === undefined || hostname === undefined) {
|
||||||
|
const header = item.getElementsByClassName('title')[0]
|
||||||
|
header.innerHTML = title
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = Array.from(pageItemBox.childNodes)
|
||||||
|
|
||||||
|
for (const c of children) {
|
||||||
|
if (typeof c == 'object' && c.time < item.time) {
|
||||||
|
try {
|
||||||
|
pageItemBox.insertBefore(item, c)
|
||||||
|
return
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
console.log(c)
|
||||||
|
clearInterval(loader_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageItemBox.appendChild(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeSince(date) {
|
||||||
|
|
||||||
|
const now = Date.now() / 1000
|
||||||
|
const seconds = Math.floor(now - date)
|
||||||
|
|
||||||
|
if (seconds > 31536000) {
|
||||||
|
return Math.floor(seconds / 31536000) + " years"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds > 2592000) {
|
||||||
|
return Math.floor(seconds / 2592000) + " months"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds > 86400) {
|
||||||
|
return Math.floor(seconds / 86400) + " days"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds > 3600) {
|
||||||
|
return Math.floor(seconds / 3600) + " hours"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds > 60) {
|
||||||
|
return Math.floor(seconds / 60) + " minutes"
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(seconds) + " seconds"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function itemPage() {
|
||||||
|
|
||||||
|
headerSub.innerHTML = ''
|
||||||
|
|
||||||
|
const item = await getItem(params.get('id'))
|
||||||
|
|
||||||
|
if (item == null || item == undefined) {
|
||||||
|
const element_string =`
|
||||||
|
<div class="block">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<h4 class="title is-4">Something went Bad :(</a></h4>
|
||||||
|
<p class="subtitle is-5">Couldn't get your result from the api</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<sep>
|
||||||
|
`
|
||||||
|
const item = document.createElement('div')
|
||||||
|
item.innerHTML = element_string
|
||||||
|
|
||||||
|
pageItemBox.appendChild(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = item.type
|
||||||
|
let target = pageItemBox
|
||||||
|
|
||||||
|
if (type != itemType.comment && type != itemType.pollopt) {
|
||||||
|
if (item.url) {
|
||||||
|
headerSub.innerHTML = `<a href=${item.url}>${item.title}</a> | ${item.score} points`
|
||||||
|
} else {
|
||||||
|
headerSub.innerHTML = `${item.title} | ${item.score} points`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type == itemType.poll) {
|
||||||
|
await loadPollOpt(item.parts)
|
||||||
|
} else if (type == itemType.pollopt){
|
||||||
|
return loadPollOpt([item.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.kids) {
|
||||||
|
console.log(type)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == itemType.comment) {
|
||||||
|
loadCommnets([item.id], target)
|
||||||
|
} else {
|
||||||
|
loadCommnets(item.kids, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPollOpt(opts) {
|
||||||
|
|
||||||
|
for (const id of opts) {
|
||||||
|
|
||||||
|
const item = await getItem(id)
|
||||||
|
|
||||||
|
if (!item || !item.text || !item.score) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const element_string =`
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-narrow is-center">
|
||||||
|
<span class="icon is-small is-left" style="height: 100%">
|
||||||
|
<i class="fas fa-arrow-up"> ${item.score}</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<h4 class="title is-4">${item.text}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
const element = document.createElement('div')
|
||||||
|
element.className = 'block'
|
||||||
|
element.innerHTML = element_string
|
||||||
|
|
||||||
|
pageItemBox.appendChild(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageItemBox.innerHTML += '<hr>'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise(resolve => {
|
||||||
|
const reachedEnd = () => {
|
||||||
|
const maxScroll = document.documentElement.scrollHeight - document.documentElement.clientHeight
|
||||||
|
return window.scrollY >= maxScroll - 100
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitForScroll = () => {
|
||||||
|
if (!reachedEnd()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
document.removeEventListener('scroll', waitForScroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reachedEnd()) {
|
||||||
|
setTimeout(resolve, ms)
|
||||||
|
} else {
|
||||||
|
document.addEventListener('scroll', waitForScroll)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadCommnets(ids, target) {
|
||||||
|
|
||||||
|
const message = (msg) => `
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="content">
|
||||||
|
${msg}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
await sleep(timer)
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const itemObj = await getItem(id)
|
||||||
|
if (itemObj == undefined || itemObj.type != itemType.comment || !itemObj.text) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = document.createElement('div')
|
||||||
|
item.className = 'card'
|
||||||
|
item.innerHTML = message(itemObj.text)
|
||||||
|
|
||||||
|
target.appendChild(item)
|
||||||
|
|
||||||
|
if (itemObj.kids && itemObj.kids.length > 0) {
|
||||||
|
const mainBox = item.getElementsByClassName('content')[0]
|
||||||
|
const subBox = document.createElement('div')
|
||||||
|
|
||||||
|
subBox.className = 'card'
|
||||||
|
|
||||||
|
mainBox.innerHTML += '<br>'
|
||||||
|
mainBox.appendChild(subBox)
|
||||||
|
|
||||||
|
await loadCommnets(itemObj.kids, subBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[loadCommnets] failed to load comment ${id}, error ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isQuery) {
|
||||||
|
newMode ? fetchMaxItem() : fetchStories()
|
||||||
|
document.addEventListener('scroll', loadItems)
|
||||||
|
} else {
|
||||||
|
itemPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================ new items section stuff ===================
|
||||||
|
let permission = 'denied'
|
||||||
|
let notificationBtn = document.getElementById('notification-btn')
|
||||||
|
|
||||||
|
notificationBtn.addEventListener("click", updatePermission)
|
||||||
|
function updatePermission() {
|
||||||
|
Notification.requestPermission().then(result => {
|
||||||
|
permission = result
|
||||||
|
notificationBtn.disabled = permission == 'denied'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
updatePermission()
|
||||||
|
|
||||||
|
let switchBtn = document.getElementById('switch-btn')
|
||||||
|
if (newMode) {
|
||||||
|
switchBtn.innerText = 'Top'
|
||||||
|
switchBtn.className = 'button is-primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
switchBtn.addEventListener("click", () => {
|
||||||
|
if (newMode) {
|
||||||
|
window.location.assign('index.html')
|
||||||
|
} else {
|
||||||
|
window.location.assign('index.html?order=new')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
Loading…
Reference in New Issue