refactor: move lots of logic to hooks, declutter (#22)
* refactor: move lots of logic to hooks, declutter * refactor: useNavigation, deprecate useAppState entirely * fix: staleTime is a number * fix: standard html mode * fix: useSettings with safe defaults * fix: terminal slash in all accepted URLs, redirecting * infra: redirects * infra: redirects from config
This commit is contained in:
parent
c67477ee1b
commit
0d06488d13
17 changed files with 378 additions and 292 deletions
|
@ -7,3 +7,8 @@
|
|||
targetPort = 1234
|
||||
port = 8080
|
||||
framework = "parcel"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
|
|
@ -41,7 +41,8 @@
|
|||
"@material-ui/core": "^4.12.1",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"htmlparser2": "^6.1.0",
|
||||
"preact": "^10.5.14"
|
||||
"preact": "^10.5.14",
|
||||
"react-query": "^3.34.16"
|
||||
},
|
||||
"alias": {
|
||||
"react": "preact/compat",
|
||||
|
|
44
src/App.tsx
44
src/App.tsx
|
@ -1,50 +1,18 @@
|
|||
import { FunctionComponent } from 'preact'
|
||||
import { useEffect } from 'preact/hooks'
|
||||
|
||||
import fetchFeeds from './utils/fetchFeeds'
|
||||
import useNavigation, { routes } from './hooks/useNavigation'
|
||||
import NavigationBar from './NavigationBar'
|
||||
import FeedsPanel from './FeedsPanel'
|
||||
import SettingsPanel from './SettingsPanel'
|
||||
import { panelIdentifiers } from './constants'
|
||||
import { restoreSettings } from './utils/persistence'
|
||||
import useAppState from './utils/useAppState'
|
||||
|
||||
export default function App(): FunctionComponent<> {
|
||||
const [state, actions] = useAppState()
|
||||
const { setActivePanel, setFeeds, setFeedUrls } = actions
|
||||
|
||||
useEffect(() => {
|
||||
if (state.loaded) return
|
||||
|
||||
setFeedUrls(restoreSettings()?.feedUrls ?? [])
|
||||
}, [state.loaded, setFeedUrls])
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
const feeds = await fetchFeeds(state.feedUrls)
|
||||
setFeeds(feeds)
|
||||
}
|
||||
|
||||
fetch()
|
||||
}, [state.feedUrls, setFeeds])
|
||||
|
||||
if (!state.feeds) return 'loading'
|
||||
export default function App(): FunctionComponent {
|
||||
const { location } = useNavigation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationBar
|
||||
activePanel={state.activePanel}
|
||||
setActivePanel={setActivePanel}
|
||||
/>
|
||||
{state.activePanel === panelIdentifiers.FEEDS ? (
|
||||
<FeedsPanel feeds={state.feeds} />
|
||||
) : null}
|
||||
{state.activePanel === panelIdentifiers.SETTINGS ? (
|
||||
<SettingsPanel
|
||||
feedUrls={state.feedUrls}
|
||||
setFeedUrls={setFeedUrls}
|
||||
/>
|
||||
) : null}
|
||||
<NavigationBar />
|
||||
{location === routes.FEEDS ? <FeedsPanel /> : null}
|
||||
{location === routes.SETTINGS ? <SettingsPanel /> : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,12 +4,10 @@ import Typography from '@material-ui/core/Typography'
|
|||
import Card from '@material-ui/core/Card'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
|
||||
import useSettings from './hooks/useSettings'
|
||||
import useRSSFeeds from './hooks/useRSSFeeds'
|
||||
import sortFeedItemsByDate from './utils/sortFeedItemsByDate'
|
||||
import type { Feed } from './types'
|
||||
|
||||
interface Props {
|
||||
feeds: Feed[]
|
||||
}
|
||||
interface CardProps {
|
||||
title: string
|
||||
url: string
|
||||
|
@ -59,8 +57,10 @@ function NoItemsNotice() {
|
|||
)
|
||||
}
|
||||
|
||||
export default function FeedsPanel(props: Props): FunctionComponent<Props> {
|
||||
const { feeds } = props
|
||||
export default function FeedsPanel(): FunctionComponent {
|
||||
const { getSettings } = useSettings()
|
||||
const settings = getSettings()
|
||||
const { feeds } = useRSSFeeds(settings.feedUrls)
|
||||
|
||||
const flattenedItems = sortFeedItemsByDate(feeds)
|
||||
|
||||
|
|
|
@ -5,32 +5,23 @@ import Typography from '@material-ui/core/Typography'
|
|||
import Button from '@material-ui/core/Button'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
|
||||
import { panelIdentifiers, readablePanelIdentifiers } from './constants'
|
||||
|
||||
interface NavigationBarProps {
|
||||
activePanel: string
|
||||
setActivePanel: (s: string) => void
|
||||
}
|
||||
import useNavigation, { routes } from './hooks/useNavigation'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
offset: theme.mixins.toolbar,
|
||||
title: { flexGrow: 1 },
|
||||
}))
|
||||
|
||||
function getAvailableDestinations(activePanel: string): string[] {
|
||||
return Object.values(panelIdentifiers).filter(
|
||||
(label) => activePanel !== label,
|
||||
)
|
||||
const routePrettyNames = {
|
||||
[routes.SETTINGS]: 'Settings',
|
||||
[routes.FEEDS]: 'Your feeds',
|
||||
}
|
||||
|
||||
export default function NavigationBar(
|
||||
props: NavigationBarProps,
|
||||
): FunctionComponent<NavigationBarProps> {
|
||||
const { activePanel, setActivePanel } = props
|
||||
export default function NavigationBar(): FunctionComponent {
|
||||
const { location, navigate } = useNavigation()
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
const availableDestinations = getAvailableDestinations(activePanel)
|
||||
return (
|
||||
<>
|
||||
<AppBar position="fixed">
|
||||
|
@ -40,17 +31,22 @@ export default function NavigationBar(
|
|||
variant="h6"
|
||||
className={classes.title}
|
||||
>
|
||||
{readablePanelIdentifiers[activePanel]}
|
||||
{routePrettyNames[location]}
|
||||
</Typography>
|
||||
{availableDestinations.map((panelIdentifier) => (
|
||||
<Button
|
||||
key={`navigation_${panelIdentifier}`}
|
||||
color="inherit"
|
||||
onClick={() => setActivePanel(panelIdentifier)}
|
||||
>
|
||||
{readablePanelIdentifiers[panelIdentifier]}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
key={'navigation-feeds'}
|
||||
color="inherit"
|
||||
onClick={() => navigate(routes.FEEDS)}
|
||||
>
|
||||
{routePrettyNames[routes.FEEDS]}
|
||||
</Button>
|
||||
<Button
|
||||
key={'navigation-settings'}
|
||||
color="inherit"
|
||||
onClick={() => navigate(routes.SETTINGS)}
|
||||
>
|
||||
{routePrettyNames[routes.SETTINGS]}
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<div className={classes.offset} />
|
||||
|
|
|
@ -9,12 +9,7 @@ import Button from '@material-ui/core/Button'
|
|||
import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'
|
||||
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
|
||||
|
||||
import { storeSettings } from './utils/persistence'
|
||||
|
||||
interface Props {
|
||||
feedUrls: string[]
|
||||
setFeedUrls: (s: string[]) => void
|
||||
}
|
||||
import useSettings from './hooks/useSettings'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
urlCard: {
|
||||
|
@ -32,9 +27,10 @@ function isValidUrl(url: string): boolean {
|
|||
return urlPattern.test(url)
|
||||
}
|
||||
|
||||
export default function SettingsPanel(props: Props): FunctionComponent<Props> {
|
||||
const { feedUrls, setFeedUrls } = props
|
||||
const [feedUrlsForm, setFeedUrlsForm] = useState(feedUrls)
|
||||
export default function SettingsPanel(): FunctionComponent {
|
||||
const { getSettings, setSettings } = useSettings()
|
||||
const settings = getSettings()
|
||||
const [feedUrlsForm, setFeedUrlsForm] = useState(settings.feedUrls)
|
||||
|
||||
const classes = useStyles()
|
||||
const urlCards = feedUrlsForm.map((url) => (
|
||||
|
@ -72,9 +68,8 @@ export default function SettingsPanel(props: Props): FunctionComponent<Props> {
|
|||
color="primary"
|
||||
onClick={() => {
|
||||
const validUrls = feedUrlsForm.filter(isValidUrl)
|
||||
setFeedUrls(validUrls)
|
||||
setSettings<string[]>('feedUrls', validUrls)
|
||||
setFeedUrlsForm(validUrls)
|
||||
storeSettings({ feedUrls: validUrls })
|
||||
}}
|
||||
>
|
||||
Save
|
||||
|
|
26
src/hooks/useLocalStorage.ts
Normal file
26
src/hooks/useLocalStorage.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
interface LocalStorageHookReturnType {
|
||||
getValue: <T>(k: string) => T
|
||||
setValue: <T>(k: string, v: T) => void
|
||||
}
|
||||
|
||||
/*
|
||||
* Encapsulates Local Storage interactions.
|
||||
*/
|
||||
export default function useLocalStorage(
|
||||
{ isJSON }: { isJSON: boolean } = { isJSON: false },
|
||||
): LocalStorageHookReturnType {
|
||||
if (!window.localStorage) throw new Error('Local Storage is not available')
|
||||
|
||||
return {
|
||||
getValue: <T>(key: string): T => {
|
||||
const raw = window.localStorage.getItem(key)
|
||||
return (isJSON ? JSON.parse(raw) : raw) as T
|
||||
},
|
||||
setValue: <T>(key: string, value: T) => {
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
(isJSON ? JSON.stringify(value) : value) as string,
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
54
src/hooks/useNavigation.tsx
Normal file
54
src/hooks/useNavigation.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { ComponentChildren, FunctionComponent, createContext } from 'preact'
|
||||
import { useCallback, useContext, useState } from 'preact/hooks'
|
||||
|
||||
interface INavigationContext {
|
||||
location: string
|
||||
navigate: (url: string) => void
|
||||
}
|
||||
|
||||
const NavigationContext = createContext(null)
|
||||
|
||||
export default function useNavigation(): INavigationContext {
|
||||
const context = useContext(NavigationContext)
|
||||
|
||||
if (!context) throw Error('Invalid navigation context')
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const routes = {
|
||||
FEEDS: '/feeds/',
|
||||
SETTINGS: '/settings/',
|
||||
}
|
||||
|
||||
export function NavigationProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ComponentChildren
|
||||
}): FunctionComponent<{ children: ComponentChildren }> {
|
||||
const [location, setLocation] = useState<string>(window.location.pathname)
|
||||
|
||||
const navigate = useCallback(
|
||||
(url: string) => {
|
||||
window.history.pushState({}, '', url)
|
||||
setLocation(url)
|
||||
},
|
||||
[setLocation],
|
||||
)
|
||||
|
||||
const suffixedLocation = location.endsWith('/') ? location : `${location}/`
|
||||
|
||||
if (suffixedLocation !== location) {
|
||||
window.history.replaceState({}, '', suffixedLocation)
|
||||
setLocation(suffixedLocation)
|
||||
}
|
||||
|
||||
if (!Object.values(routes).includes(suffixedLocation))
|
||||
navigate(routes.FEEDS)
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider value={{ location, navigate }}>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
)
|
||||
}
|
97
src/hooks/useRSSFeeds.ts
Normal file
97
src/hooks/useRSSFeeds.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { parseFeed } from 'htmlparser2'
|
||||
import { useQueries } from 'react-query'
|
||||
|
||||
import { Feed } from '../types'
|
||||
|
||||
import useLocalStorage from './useLocalStorage'
|
||||
|
||||
function mergeFeeds(first, second) {
|
||||
// Assuming `second` is newer.
|
||||
const seen = new Set(first.items.map((item) => item.url))
|
||||
const mergedItems = second.items.reduce(
|
||||
(updatedItems, item) => {
|
||||
if (!seen.has(item.url)) {
|
||||
updatedItems.push(item)
|
||||
seen.add(item.url)
|
||||
}
|
||||
|
||||
return updatedItems
|
||||
},
|
||||
[...first.items],
|
||||
)
|
||||
return {
|
||||
...second,
|
||||
items: mergedItems,
|
||||
}
|
||||
}
|
||||
|
||||
function processFeedXML(feed): Feed {
|
||||
return {
|
||||
title: feed.title,
|
||||
lastPull: String(Date.now()),
|
||||
items: feed.items.reduce((items, feedItem) => {
|
||||
items.push({
|
||||
title: feedItem.title,
|
||||
url: feedItem.link,
|
||||
published: new Date(feedItem.pubDate),
|
||||
})
|
||||
|
||||
return items
|
||||
}, []),
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFeed(
|
||||
url: string,
|
||||
persistedData: Feed | null,
|
||||
): Promise<Feed> {
|
||||
const response = await fetch(`/.netlify/functions/rss-proxy?url=${url}`)
|
||||
|
||||
const responseData = await response.text()
|
||||
|
||||
try {
|
||||
const newFeedData = parseFeed(responseData)
|
||||
const newFeed = processFeedXML(newFeedData)
|
||||
const mergedFeeds = persistedData
|
||||
? mergeFeeds(persistedData, newFeed)
|
||||
: newFeed
|
||||
|
||||
return mergedFeeds
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
return persistedData
|
||||
}
|
||||
|
||||
export default function useRSSFeeds(urls: string[]): { feeds: Feed[] } {
|
||||
const { getValue, setValue } = useLocalStorage({ isJSON: true })
|
||||
|
||||
const queries = useQueries(
|
||||
urls.map((feedUrl: string) => {
|
||||
const localStorageKey = `feed_${feedUrl}`
|
||||
const persistedData = getValue<Feed>(localStorageKey)
|
||||
|
||||
return {
|
||||
queryKey: ['feed', feedUrl],
|
||||
queryFn: () => fetchFeed(feedUrl, persistedData),
|
||||
staleTime: 30000,
|
||||
onSuccess: (data: Feed) => {
|
||||
setValue(localStorageKey, data)
|
||||
},
|
||||
initialData: persistedData ?? undefined,
|
||||
initialDataUpdatedAt:
|
||||
Number(persistedData?.lastPull) ?? undefined,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const fetchedFeeds = queries.reduce((fetchedFeeds: Feed[], current) => {
|
||||
if (current.isSuccess) fetchedFeeds.push(current.data)
|
||||
|
||||
return fetchedFeeds
|
||||
}, [])
|
||||
|
||||
return { feeds: fetchedFeeds }
|
||||
}
|
25
src/hooks/useSettings.ts
Normal file
25
src/hooks/useSettings.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Settings } from '../types'
|
||||
|
||||
import useLocalStorage from './useLocalStorage'
|
||||
|
||||
const defaultSettings = {
|
||||
feedUrls: [],
|
||||
}
|
||||
|
||||
export default function useSettings(): {
|
||||
getSettings: () => Settings
|
||||
setSettings: <T>(k: string, value: T) => void
|
||||
} {
|
||||
const { getValue, setValue } = useLocalStorage({ isJSON: true })
|
||||
|
||||
const getSettings = (): Settings =>
|
||||
getValue<Settings>('settings') ?? defaultSettings
|
||||
|
||||
const setSettings = <T>(key: string, value: T) => {
|
||||
const current = getSettings()
|
||||
|
||||
setValue<Settings>('settings', { ...current, [key]: value })
|
||||
}
|
||||
|
||||
return { getSettings, setSettings }
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>🏴☠️ Yet Another RSS Reader 🏴☠️</title>
|
||||
|
|
|
@ -2,8 +2,19 @@ if (process.env.NODE_ENV === 'development') {
|
|||
require('preact/debug')
|
||||
}
|
||||
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
import { render } from 'preact'
|
||||
|
||||
import { NavigationProvider } from './hooks/useNavigation'
|
||||
import App from './App'
|
||||
|
||||
render(<App />, document.getElementById('app'))
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
render(
|
||||
<NavigationProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</NavigationProvider>,
|
||||
document.getElementById('app'),
|
||||
)
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
import { parseFeed } from 'htmlparser2'
|
||||
|
||||
import type { Feed } from '../types'
|
||||
|
||||
import { restoreRssData, storeRssData } from './persistence'
|
||||
|
||||
function processFeedXML(feed) {
|
||||
return {
|
||||
title: feed.title,
|
||||
lastPull: Date.now(),
|
||||
items: feed.items.reduce((items, feedItem) => {
|
||||
items.push({
|
||||
title: feedItem.title,
|
||||
url: feedItem.link,
|
||||
published: new Date(feedItem.pubDate),
|
||||
})
|
||||
|
||||
return items
|
||||
}, []),
|
||||
}
|
||||
}
|
||||
|
||||
function getRefetchThreshold() {
|
||||
const refetchThreshold = new Date()
|
||||
refetchThreshold.setMinutes(refetchThreshold.getMinutes() - 10)
|
||||
return refetchThreshold
|
||||
}
|
||||
|
||||
function mergeFeeds(first, second) {
|
||||
// Assuming `second` is newer.
|
||||
const seen = new Set(first.items.map((item) => item.url))
|
||||
const mergedItems = second.items.reduce(
|
||||
(updatedItems, item) => {
|
||||
if (!seen.has(item.url)) {
|
||||
updatedItems.push(item)
|
||||
seen.add(item.url)
|
||||
}
|
||||
|
||||
return updatedItems
|
||||
},
|
||||
[...first.items],
|
||||
)
|
||||
return {
|
||||
...second,
|
||||
items: mergedItems,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
* items are added on top. If the cache has been updated in the past
|
||||
* 10 minutes, no network fetch happens.
|
||||
*
|
||||
* Returns a list of Item.
|
||||
*/
|
||||
export default async function fetchFeeds(
|
||||
feedUrls: string[],
|
||||
forceRefetch = false,
|
||||
): Feed[] {
|
||||
const feeds = await Promise.all(
|
||||
feedUrls.map(async (url: string) => {
|
||||
const storedFeedData = restoreRssData(url)
|
||||
|
||||
// Skip refetch if not stale / not forced
|
||||
const lastPull = storedFeedData?.lastPull
|
||||
if (!forceRefetch && lastPull > getRefetchThreshold())
|
||||
return storedFeedData
|
||||
|
||||
const response = await fetch(
|
||||
`/.netlify/functions/rss-proxy?url=${url}`,
|
||||
)
|
||||
|
||||
if (!response.ok) return storedFeedData
|
||||
|
||||
const responseData = await response.text()
|
||||
|
||||
try {
|
||||
const newFeedData = parseFeed(responseData)
|
||||
const newFeed = processFeedXML(newFeedData)
|
||||
const mergedFeeds = storedFeedData
|
||||
? mergeFeeds(storedFeedData, newFeed)
|
||||
: newFeed
|
||||
|
||||
storeRssData(url, mergedFeeds)
|
||||
return mergedFeeds
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
return storedFeedData
|
||||
}),
|
||||
)
|
||||
|
||||
return feeds.filter((feed) => feed)
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import type { RSSData, Settings } from '../types'
|
||||
|
||||
const SETTINGS_KEY = 'settings'
|
||||
const RSS_DATA_KEY_PREFIX = 'savedItems_'
|
||||
|
||||
function cacheKeyFromURL(url: string): string {
|
||||
return url.replace(/^https?:\/\//, '').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function storeToLocal(key: string, data): void {
|
||||
window.localStorage.setItem(key, JSON.stringify(data))
|
||||
}
|
||||
|
||||
function restoreFromLocal(key: string) {
|
||||
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 restoreSettings(): Settings {
|
||||
return restoreFromLocal(SETTINGS_KEY)
|
||||
}
|
||||
|
||||
export function storeRssData(url: string, items: Item[]): void {
|
||||
const key = RSS_DATA_KEY_PREFIX + cacheKeyFromURL(url)
|
||||
storeToLocal(key, items)
|
||||
}
|
||||
|
||||
export function restoreRssData(url: string): RSSData {
|
||||
const key = RSS_DATA_KEY_PREFIX + cacheKeyFromURL(url)
|
||||
return restoreFromLocal(key)
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import { useReducer, useRef } from 'preact/hooks'
|
||||
|
||||
import type { State } from '../types'
|
||||
import { panelIdentifiers } from '../constants'
|
||||
|
||||
export const SET_PANEL_IDENTIFIER = 'setPanelIdentifier'
|
||||
export const SET_FEED_URLS = 'setFeedUrls'
|
||||
export const SET_FEEDS = 'setFeeds'
|
||||
|
||||
export const defaultState = {
|
||||
loaded: false,
|
||||
activePanel: panelIdentifiers.FEEDS,
|
||||
feeds: [],
|
||||
feedUrls: [],
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case SET_PANEL_IDENTIFIER:
|
||||
return { ...state, activePanel: action.payload.panelIdentifier }
|
||||
case SET_FEED_URLS:
|
||||
return { ...state, loaded: true, feedUrls: action.payload.feedUrls }
|
||||
case SET_FEEDS:
|
||||
return { ...state, feeds: action.payload.feeds }
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function setActivePanel(panelIdentifier: string) {
|
||||
return {
|
||||
type: SET_PANEL_IDENTIFIER,
|
||||
payload: { panelIdentifier },
|
||||
}
|
||||
}
|
||||
|
||||
function setFeedUrls(feedUrls: string[]) {
|
||||
return {
|
||||
type: SET_FEED_URLS,
|
||||
payload: { feedUrls },
|
||||
}
|
||||
}
|
||||
|
||||
function setFeeds(feeds) {
|
||||
return {
|
||||
type: SET_FEEDS,
|
||||
payload: { feeds },
|
||||
}
|
||||
}
|
||||
|
||||
type Action =
|
||||
| ReturnType<setFeeds>
|
||||
| ReturnType<setFeedUrls>
|
||||
| ReturnType<setActivePanel>
|
||||
|
||||
interface ActionSet {
|
||||
[key: string]: Action
|
||||
}
|
||||
|
||||
export default function useAppState(): [State, ActionSet] {
|
||||
const [state, dispatch] = useReducer(reducer, defaultState)
|
||||
|
||||
const actions = {
|
||||
setActivePanel: (panelIdentifier) =>
|
||||
dispatch(setActivePanel(panelIdentifier)),
|
||||
setFeedUrls: (feedUrls) => dispatch(setFeedUrls(feedUrls)),
|
||||
setFeeds: (feeds) => dispatch(setFeeds(feeds)),
|
||||
}
|
||||
|
||||
const stableActions = useRef(actions)
|
||||
|
||||
return [state, stableActions.current]
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"target": "es2017",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"moduleResolution": "node"
|
||||
|
|
117
yarn.lock
117
yarn.lock
|
@ -1328,6 +1328,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2":
|
||||
version: 7.17.2
|
||||
resolution: "@babel/runtime@npm:7.17.2"
|
||||
dependencies:
|
||||
regenerator-runtime: ^0.13.4
|
||||
checksum: a48702d271ecc59c09c397856407afa29ff980ab537b3da58eeee1aeaa0f545402d340a1680c9af58aec94dfdcbccfb6abb211991b74686a86d03d3f6956cacd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.8.4":
|
||||
version: 7.14.8
|
||||
resolution: "@babel/runtime@npm:7.14.8"
|
||||
|
@ -5095,6 +5104,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"big-integer@npm:^1.6.16":
|
||||
version: 1.6.51
|
||||
resolution: "big-integer@npm:1.6.51"
|
||||
checksum: 3d444173d1b2e20747e2c175568bedeebd8315b0637ea95d75fd27830d3b8e8ba36c6af40374f36bdaea7b5de376dcada1b07587cb2a79a928fccdb6e6e3c518
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"big.js@npm:^5.2.2":
|
||||
version: 5.2.2
|
||||
resolution: "big.js@npm:5.2.2"
|
||||
|
@ -5245,6 +5261,22 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"broadcast-channel@npm:^3.4.1":
|
||||
version: 3.7.0
|
||||
resolution: "broadcast-channel@npm:3.7.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.7.2
|
||||
detect-node: ^2.1.0
|
||||
js-sha3: 0.8.0
|
||||
microseconds: 0.2.0
|
||||
nano-time: 1.0.0
|
||||
oblivious-set: 1.0.0
|
||||
rimraf: 3.0.2
|
||||
unload: 2.2.0
|
||||
checksum: 803794c48dcce7f03aca69797430bd8b1c4cfd70b7de22079cd89567eeffaa126a1db98c7c2d86af8131d9bb41ed367c0fef96dfb446151c927b831572c621fc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brorand@npm:^1.0.1, brorand@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "brorand@npm:1.1.0"
|
||||
|
@ -7210,6 +7242,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-node@npm:^2.0.4, detect-node@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "detect-node@npm:2.1.0"
|
||||
checksum: 832184ec458353e41533ac9c622f16c19f7c02d8b10c303dfd3a756f56be93e903616c0bb2d4226183c9351c15fc0b3dba41a17a2308262afabcfa3776e6ae6e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detective-amd@npm:^3.0.1":
|
||||
version: 3.1.0
|
||||
resolution: "detective-amd@npm:3.1.0"
|
||||
|
@ -11469,6 +11508,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-sha3@npm:0.8.0":
|
||||
version: 0.8.0
|
||||
resolution: "js-sha3@npm:0.8.0"
|
||||
checksum: 75df77c1fc266973f06cce8309ce010e9e9f07ec35ab12022ed29b7f0d9c8757f5a73e1b35aa24840dced0dea7059085aa143d817aea9e188e2a80d569d9adce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-string-escape@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "js-string-escape@npm:1.0.1"
|
||||
|
@ -12510,6 +12556,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"match-sorter@npm:^6.0.2":
|
||||
version: 6.3.1
|
||||
resolution: "match-sorter@npm:6.3.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.12.5
|
||||
remove-accents: 0.4.2
|
||||
checksum: a4b02b676ac4ce64a89a091539ee4a70a802684713bcf06f2b70787927f510fe8a2adc849f9288857a90906083ad303467e530e8723b4a9756df9994fc164550
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"maxstache-stream@npm:^1.0.0":
|
||||
version: 1.0.4
|
||||
resolution: "maxstache-stream@npm:1.0.4"
|
||||
|
@ -12659,6 +12715,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"microseconds@npm:0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "microseconds@npm:0.2.0"
|
||||
checksum: 22bfa8553f92c7d95afff6de0aeb2aecf750680d41b8c72b02098ccc5bbbb0a384380ff539292dbd3788f5dfc298682f9d38a2b4c101f5ee2c9471d53934c5fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"miller-rabin@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "miller-rabin@npm:4.0.1"
|
||||
|
@ -12999,6 +13062,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nano-time@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "nano-time@npm:1.0.0"
|
||||
dependencies:
|
||||
big-integer: ^1.6.16
|
||||
checksum: eef8548546cc1020625f8e44751a7263e9eddf0412a6a1a6c80a8d2be2ea7973622804a977cdfe796807b85b20ff6c8ba340e8dd20effcc7078193ed5edbb5d4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nanocolors@npm:^0.1.12":
|
||||
version: 0.1.12
|
||||
resolution: "nanocolors@npm:0.1.12"
|
||||
|
@ -13621,6 +13693,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oblivious-set@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "oblivious-set@npm:1.0.0"
|
||||
checksum: f31740ea9c3a8242ad2324e4ebb9a35359fbc2e6e7131731a0fc1c8b7b1238eb07e4c8c631a38535243a7b8e3042b7e89f7dc2a95d2989afd6f80bd5793b0aab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oclif-plugin-completion@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "oclif-plugin-completion@npm:0.6.0"
|
||||
|
@ -15421,6 +15500,24 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-query@npm:^3.34.16":
|
||||
version: 3.34.16
|
||||
resolution: "react-query@npm:3.34.16"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.5.5
|
||||
broadcast-channel: ^3.4.1
|
||||
match-sorter: ^6.0.2
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
checksum: 77f5ee7279fa82642d5210b785f7b9eccc6e81c5b27b59bf112101b14e90737efeb8b73f9597bff56a08c5792ec5fa6889e14913aa196f5086ff4faeb077abd8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-refresh@npm:^0.9.0":
|
||||
version: 0.9.0
|
||||
resolution: "react-refresh@npm:0.9.0"
|
||||
|
@ -15677,6 +15774,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"remove-accents@npm:0.4.2":
|
||||
version: 0.4.2
|
||||
resolution: "remove-accents@npm:0.4.2"
|
||||
checksum: 84a6988555dea24115e2d1954db99509588d43fe55a1590f0b5894802776f7b488b3151c37ceb9e4f4b646f26b80b7325dcea2fae58bc3865df146e1fa606711
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"remove-trailing-separator@npm:^1.0.1":
|
||||
version: 1.1.0
|
||||
resolution: "remove-trailing-separator@npm:1.1.0"
|
||||
|
@ -15935,7 +16039,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:^3.0.0, rimraf@npm:^3.0.2":
|
||||
"rimraf@npm:3.0.2, rimraf@npm:^3.0.0, rimraf@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "rimraf@npm:3.0.2"
|
||||
dependencies:
|
||||
|
@ -16041,6 +16145,7 @@ __metadata:
|
|||
parcel: ^2.0.0
|
||||
preact: ^10.5.14
|
||||
prettier: ^2.3.2
|
||||
react-query: ^3.34.16
|
||||
typescript: ^4.3.5
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
@ -17912,6 +18017,16 @@ typescript@^3.9.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unload@npm:2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "unload@npm:2.2.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
detect-node: ^2.0.4
|
||||
checksum: 88ba950c5ff83ab4f9bbd8f63bbf19ba09687ed3c434efd43b7338cc595bc574df8f9b155ee6eee7a435de3d3a4a226726988428977a68ba4907045f1fac5d41
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unpipe@npm:1.0.0, unpipe@npm:~1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "unpipe@npm:1.0.0"
|
||||
|
|
Loading…
Reference in a new issue