fix: initial load #3
9 changed files with 89 additions and 28 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -115,3 +115,6 @@ dist
|
||||||
|
|
||||||
# TernJS port file
|
# TernJS port file
|
||||||
.tern-port
|
.tern-port
|
||||||
|
|
||||||
|
# Local Netlify folder
|
||||||
|
.netlify
|
24
netlify/functions/rss-proxy/rss-proxy.js
Executable file
24
netlify/functions/rss-proxy/rss-proxy.js
Executable file
|
@ -0,0 +1,24 @@
|
||||||
|
const https = require('https')
|
||||||
|
|
||||||
|
async function httpGet(url) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
https.get(url, (response) => {
|
||||||
|
response.on('data', (d) => resolve(d))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = async (event) => {
|
||||||
|
try {
|
||||||
|
const url = event.queryStringParameters.url
|
||||||
|
const proxiedResponse = await httpGet(url)
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
body: String(proxiedResponse),
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { statusCode: 500, body: error.toString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { handler }
|
|
@ -6,7 +6,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "parcel src/index.html --https --host localhost.localdomain",
|
"start": "parcel src/index.html --https --host localhost.localdomain",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"lint:fix": "eslint src --fix",
|
"lint:fix": "eslint src netlify --fix",
|
||||||
"types": "tsc --noEmit",
|
"types": "tsc --noEmit",
|
||||||
"build": "parcel build src/index.html",
|
"build": "parcel build src/index.html",
|
||||||
"build:bundlesize": "parcel build src/index.html --reporter @parcel/reporter-bundle-analyzer"
|
"build:bundlesize": "parcel build src/index.html --reporter @parcel/reporter-bundle-analyzer"
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default function App(): ReactNode {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.loaded) return
|
if (state.loaded) return
|
||||||
|
|
||||||
setFeedUrls(restoreSettings().feedUrls)
|
setFeedUrls(restoreSettings()?.feedUrls ?? [])
|
||||||
}, [state.loaded, setFeedUrls])
|
}, [state.loaded, setFeedUrls])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import Button from '@material-ui/core/Button'
|
||||||
import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'
|
import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'
|
||||||
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
|
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
|
||||||
|
|
||||||
import { persistSettings } from './utils/persistence'
|
import { storeSettings } from './utils/persistence'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
feedUrls: string[]
|
feedUrls: string[]
|
||||||
|
@ -73,7 +73,7 @@ export default function SettingsPanel(props: Props): ReactNode {
|
||||||
const validUrls = feedUrlsForm.filter(isValidUrl)
|
const validUrls = feedUrlsForm.filter(isValidUrl)
|
||||||
setFeedUrls(validUrls)
|
setFeedUrls(validUrls)
|
||||||
setFeedUrlsForm(validUrls)
|
setFeedUrlsForm(validUrls)
|
||||||
persistSettings({ feedUrls: validUrls })
|
storeSettings({ feedUrls: validUrls })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<html>
|
<html>
|
||||||
<head></head>
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body { background-color: #f5f5f5 }
|
body { background-color: #f5f5f5 }
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,3 +10,12 @@ export interface State {
|
||||||
feedUrls: string[]
|
feedUrls: string[]
|
||||||
activePanel: string
|
activePanel: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RSSData {
|
||||||
|
items: Item[]
|
||||||
|
lastPushed: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
feedUrls: string[]
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { parseFeed } from 'htmlparser2'
|
import { parseFeed } from 'htmlparser2'
|
||||||
import md5 from 'crypto-js/md5'
|
|
||||||
|
|
||||||
import type { Item } from '../types'
|
import type { Item } from '../types'
|
||||||
|
|
||||||
import { persistResults, restoreResults } from './persistence'
|
import { restoreRssData, storeRssData } from './persistence'
|
||||||
|
|
||||||
function processFeedXML(feed) {
|
function processFeedXML(feed) {
|
||||||
return feed.items.reduce((items, feedItem) => {
|
return feed.items.reduce((items, feedItem) => {
|
||||||
|
@ -18,6 +17,12 @@ function processFeedXML(feed) {
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRefetchThreshold() {
|
||||||
|
const refetchThreshold = new Date()
|
||||||
|
refetchThreshold.setMinutes(refetchThreshold.getMinutes() - 10)
|
||||||
|
return refetchThreshold
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Fetches RSS feeds from the given url list. If feed data exists in
|
* Fetches RSS feeds from the given url list. If feed data exists in
|
||||||
* localStorage, it is used as a basis for the final list and any new
|
* localStorage, it is used as a basis for the final list and any new
|
||||||
|
@ -32,17 +37,16 @@ export default async function fetchFeeds(
|
||||||
): Item[] {
|
): Item[] {
|
||||||
const feed = await Promise.all(
|
const feed = await Promise.all(
|
||||||
feedUrls.map(async (url: string) => {
|
feedUrls.map(async (url: string) => {
|
||||||
const urlHash = md5(url)
|
const storedFeedData = restoreRssData(url)
|
||||||
const storedFeedData = restoreResults(urlHash)
|
|
||||||
|
|
||||||
const { items, lastPush } = storedFeedData || { items: [] }
|
const items = storedFeedData?.items || []
|
||||||
|
const lastPush = storedFeedData?.lastPush
|
||||||
|
|
||||||
// TODO: Constantize
|
if (!forceRefetch && lastPush > getRefetchThreshold()) return items
|
||||||
if (!forceRefetch && lastPush > Date.now() - 10 * 60 * 1000)
|
|
||||||
return items
|
|
||||||
|
|
||||||
const response = await axios.get(url)
|
|
||||||
|
|
||||||
|
const response = await axios.get('/.netlify/functions/rss-proxy', {
|
||||||
|
params: { url },
|
||||||
|
})
|
||||||
const availableFeedItems = [...items]
|
const availableFeedItems = [...items]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -58,9 +62,9 @@ export default async function fetchFeeds(
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(e)
|
console.error(e.response)
|
||||||
}
|
}
|
||||||
persistResults(urlHash, availableFeedItems)
|
storeRssData(url, availableFeedItems)
|
||||||
|
|
||||||
return availableFeedItems
|
return availableFeedItems
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,18 +1,36 @@
|
||||||
export function persistSettings(settings) {
|
import md5 from 'crypto-js/md5'
|
||||||
window.localStorage.setItem('settings', JSON.stringify(settings))
|
|
||||||
|
import type { RSSData, Settings } from '../types'
|
||||||
|
|
||||||
|
const SETTINGS_KEY = 'settings'
|
||||||
|
const RSS_DATA_KEY_PREFIX = 'savedItems_'
|
||||||
|
|
||||||
|
function storeToLocal(key: string, data): void {
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restoreSettings() {
|
function restoreFromLocal(key: string) {
|
||||||
return JSON.parse(window.localStorage.getItem('settings') ?? {})
|
const restored = window.localStorage.getItem(key)
|
||||||
|
try {
|
||||||
|
return JSON.parse(restored)
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function storeSettings(settings: Settings): void {
|
||||||
|
return storeToLocal(SETTINGS_KEY, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function persistResults(h: string, items: Item[]) {
|
export function restoreSettings(): Settings {
|
||||||
window.localStorage.setItem(
|
return restoreFromLocal(SETTINGS_KEY)
|
||||||
`savedItems_${h}`,
|
|
||||||
JSON.stringify({ items, lastPush: Date.now() }),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restoreResults(key): Item[] {
|
export function storeRssData(url: string, items: Item[]): void {
|
||||||
return JSON.parse(window.localStorage.getItem(`savedItems_${key}`))
|
const key = RSS_DATA_KEY_PREFIX + md5(url)
|
||||||
|
storeToLocal(key, { items, lastPush: Date.now() })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreRssData(url: string): RSSData {
|
||||||
|
const key = RSS_DATA_KEY_PREFIX + md5(url)
|
||||||
|
return restoreFromLocal(key)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue