clonernews/static/index.js

496 lines
12 KiB
JavaScript

"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 endItem // for the later new items. new items will be fetch until this item
let maxItem
let new_loader_id
// 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
})
}
async function fetchMaxItem() {
const req = await fetch('https://hacker-news.firebaseio.com/v0/maxitem.json')
return req.json()
}
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)
if (newMode && !new_loader_id) {
new_loader_id = setInterval(loadFresh, 5e3)
}
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) {
clearInterval(loader_id)
throw new Error(`[makeItemListing] null or undefined object`)
}
if (itemObj.deleted || itemObj.dead || itemObj.title == "[deleted]") {
console.debug(`[makeItemListing] Deleted item: ${itemObj.id}`)
return
}
if (itemObj.type == itemType.comment || itemObj.type == itemType.pollopt) {
console.debug(`[makeItemListing] Comment or Poll Option item: ${itemObj.id}`)
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 = "index.html?id=" + id
// i got lazy
item.time = time
item.points = points
item.comments = comments
item.author = author
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">&nbsp;${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>
</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}`)
}
}
}
async function loadPage() {
if (isQuery) {
return itemPage()
}
if (newMode) {
maxItem = await fetchMaxItem()
endItem = maxItem
} else {
await fetchStories()
}
loadItems()
document.addEventListener('scroll', loadItems)
}
loadPage()
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')
}
})
// ================ new items section stuff ===================
let permission = 'denied'
let notificationBtn = document.getElementById('notification-btn')
notificationBtn.addEventListener("click", updatePermission)
function updatePermission() {
if (!newMode) {
notificationBtn.style.visibility = 'collapse'
return
}
Notification.requestPermission().then(result => {
permission = result
notificationBtn.disabled = permission == 'denied'
})
}
updatePermission()
async function notify(count) {
if (count == 0) {
return
}
new Notification("Clone Wars", { body: `${count} new Posts`})
}
async function loadFresh() {
let newMax = endItem
try {
newMax = await fetchMaxItem()
} catch (e) {
console.log(`[loadFresh] got error ${e}`)
}
const newEnd = newMax
if (newMax <= endItem) {
return
}
console.log(`[loadFresh] New Items ${newMax - endItem}`)
const count = pageItemBox.childElementCount
let intervalId = setInterval(() => {
if (newMax <= endItem) {
endItem = newEnd
notify(pageItemBox.childElementCount - count)
clearInterval(intervalId)
return
}
try {
getItem(newMax--).then(item => {
if (item) {
makeItemListing(item)
}
})
} catch (e) {
console.log(`[loadFresh] got error ${e}`)
}
}, timer)
}
if (!isQuery) {
setInterval(() => Array.from(pageItemBox.getElementsByTagName('a')).map(c => {
if (!c.time) {
return
}
const sub = c.getElementsByClassName('subtitle')[0]
const date = timeSince(c.time)
const points = c.points
const author = c.author
const comments = c.comments
let subtitle = `${points} points by ${author} ${date} ago`
if (comments !== undefined) {
subtitle += ` | ${comments} comments`
}
sub.innerHTML = subtitle
}), 1e3)
}