build: switch linterformatter to rome (#120)
* build: replace eslint with rome * chore: run lint+formatter * chore: more cleanup
This commit is contained in:
parent
fc531cd84a
commit
19cb3dba19
17 changed files with 538 additions and 3044 deletions
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
extends: '@tophat',
|
||||
rules: {
|
||||
"react/react-in-jsx-scope": "off"
|
||||
}
|
||||
}
|
19
package.json
19
package.json
|
@ -10,8 +10,8 @@
|
|||
"scripts": {
|
||||
"start:app": "yarn workspace app start",
|
||||
"start:api": "yarn workspace api start",
|
||||
"lint": "eslint packages/**/*.ts",
|
||||
"lint:fix": "eslint packages/**/*.ts netlify --fix",
|
||||
"lint": "yarn rome format packages/**/src/*.ts && yarn rome check packages/**/src/*.ts",
|
||||
"lint:fix": "yarn rome format packages/**/src/*.ts --write && yarn rome check packages/**/src/*.ts --apply",
|
||||
"types": "tsc --noEmit",
|
||||
"clean": "rm -rf dist/*",
|
||||
"build:app": "yarn workspace app build",
|
||||
|
@ -22,25 +22,12 @@
|
|||
"devDependencies": {
|
||||
"@parcel/reporter-bundle-analyzer": "^2.9.3",
|
||||
"@parcel/validator-typescript": "^2.9.3",
|
||||
"@tophat/eslint-config": "3.3.0",
|
||||
"@tophat/eslint-import-resolver-require": "0.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.2",
|
||||
"@typescript-eslint/parser": "5.48.2",
|
||||
"esbuild": "^0.17.3",
|
||||
"eslint": "8.32.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-import-resolver-node": "0.3.6",
|
||||
"eslint-import-resolver-typescript": "3.5.2",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-jest": "27.2.1",
|
||||
"eslint-plugin-jsx-a11y": "6.6.1",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.31.10",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"jest": "29.3.1",
|
||||
"netlify-cli": "^15.7.0",
|
||||
"parcel": "^2.9.3",
|
||||
"prettier": "2.7.1",
|
||||
"rome": "^12.1.3",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"resolutions": {
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import {
|
||||
type Express,
|
||||
type Request,
|
||||
type Response,
|
||||
default as express,
|
||||
} from 'express'
|
||||
type Express,
|
||||
type Request,
|
||||
type Response,
|
||||
default as express,
|
||||
} from "express";
|
||||
|
||||
const app: Express = express()
|
||||
const app: Express = express();
|
||||
|
||||
app.get('/', (req: Request, res: Response) => {
|
||||
res.send('ok')
|
||||
})
|
||||
app.get("/", (req: Request, res: Response) => {
|
||||
res.send("ok");
|
||||
});
|
||||
|
||||
app.listen(8081, () => {})
|
||||
app.listen(8081, () => {});
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { FunctionComponent } from 'preact'
|
||||
import { FunctionComponent } from "preact";
|
||||
|
||||
import FeedsPanel from './FeedsPanel'
|
||||
import NavigationBar from './NavigationBar'
|
||||
import SettingsPanel from './SettingsPanel'
|
||||
import useNavigation, { routes } from './hooks/useNavigation'
|
||||
import FeedsPanel from "./FeedsPanel";
|
||||
import NavigationBar from "./NavigationBar";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import useNavigation, { routes } from "./hooks/useNavigation";
|
||||
|
||||
export default function App(): FunctionComponent {
|
||||
const { location } = useNavigation()
|
||||
const { location } = useNavigation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationBar />
|
||||
{location === routes.FEEDS ? <FeedsPanel /> : null}
|
||||
{location === routes.SETTINGS ? <SettingsPanel /> : null}
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<NavigationBar />
|
||||
{location === routes.FEEDS ? <FeedsPanel /> : null}
|
||||
{location === routes.SETTINGS ? <SettingsPanel /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,76 +1,75 @@
|
|||
import Box from '@material-ui/core/Box'
|
||||
import Card from '@material-ui/core/Card'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import Box from "@material-ui/core/Box";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { FunctionComponent } from "preact";
|
||||
|
||||
import useRSSFeeds from './hooks/useRSSFeeds'
|
||||
import useSettings from './hooks/useSettings'
|
||||
import sortFeedItemsByDate from './utils'
|
||||
import useRSSFeeds from "./hooks/useRSSFeeds";
|
||||
import useSettings from "./hooks/useSettings";
|
||||
import sortFeedItemsByDate from "./utils";
|
||||
|
||||
interface CardProps {
|
||||
title: string
|
||||
url: string
|
||||
published: Date
|
||||
feedTitle: string
|
||||
title: string;
|
||||
url: string;
|
||||
published: Date;
|
||||
feedTitle: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
margin: '5px',
|
||||
padding: '10px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
})
|
||||
root: {
|
||||
margin: "5px",
|
||||
padding: "10px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
});
|
||||
|
||||
function ItemCard(props: CardProps) {
|
||||
const { title, url, published, feedTitle } = props
|
||||
const classes = useStyles()
|
||||
const { title, url, published, feedTitle } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
const formattedDate = new Date(published).toLocaleString('en-GB', {
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
<a href={url}>{title}</a>
|
||||
<span>{`${feedTitle} - ${formattedDate}`}</span>
|
||||
</Card>
|
||||
)
|
||||
const formattedDate = new Date(published).toLocaleString("en-GB", {
|
||||
timeZone: "UTC",
|
||||
});
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
<a href={url}>{title}</a>
|
||||
<span>{`${feedTitle} - ${formattedDate}`}</span>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function NoItemsNotice() {
|
||||
return (
|
||||
<Box
|
||||
alignSelf="center"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
mt="50px"
|
||||
>
|
||||
<Typography variant="h6">Nothing to see here!</Typography>
|
||||
<Typography>
|
||||
Add some feeds in the <strong>Settings</strong> panel to get
|
||||
started!
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
return (
|
||||
<Box
|
||||
alignSelf="center"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
mt="50px"
|
||||
>
|
||||
<Typography variant="h6">Nothing to see here!</Typography>
|
||||
<Typography>
|
||||
Add some feeds in the <strong>Settings</strong> panel to get started!
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FeedsPanel(): FunctionComponent {
|
||||
const { getSettings } = useSettings()
|
||||
const settings = getSettings()
|
||||
const { feeds } = useRSSFeeds(settings.feedUrls)
|
||||
const { getSettings } = useSettings();
|
||||
const settings = getSettings();
|
||||
const { feeds } = useRSSFeeds(settings.feedUrls);
|
||||
|
||||
const flattenedItems = sortFeedItemsByDate(feeds)
|
||||
const flattenedItems = sortFeedItemsByDate(feeds);
|
||||
|
||||
const cardList = flattenedItems.map((item) => (
|
||||
<ItemCard {...item} key={`feed_item_${item.title.replace(' ', '_')}`} />
|
||||
))
|
||||
const cardList = flattenedItems.map((item) => (
|
||||
<ItemCard {...item} key={`feed_item_${item.title.replace(" ", "_")}`} />
|
||||
));
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column">
|
||||
{cardList.length > 0 ? cardList : <NoItemsNotice />}
|
||||
</Box>
|
||||
)
|
||||
return (
|
||||
<Box display="flex" flexDirection="column">
|
||||
{cardList.length > 0 ? cardList : <NoItemsNotice />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,55 +1,51 @@
|
|||
import AppBar from '@material-ui/core/AppBar'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Toolbar from '@material-ui/core/Toolbar'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import AppBar from "@material-ui/core/AppBar";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Toolbar from "@material-ui/core/Toolbar";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { FunctionComponent } from "preact";
|
||||
|
||||
import useNavigation, { routes } from './hooks/useNavigation'
|
||||
import useNavigation, { routes } from "./hooks/useNavigation";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
offset: theme.mixins.toolbar,
|
||||
title: { flexGrow: 1 },
|
||||
}))
|
||||
offset: theme.mixins.toolbar,
|
||||
title: { flexGrow: 1 },
|
||||
}));
|
||||
|
||||
const routePrettyNames = {
|
||||
[routes.SETTINGS]: 'Settings',
|
||||
[routes.FEEDS]: 'Your feeds',
|
||||
}
|
||||
[routes.SETTINGS]: "Settings",
|
||||
[routes.FEEDS]: "Your feeds",
|
||||
};
|
||||
|
||||
export default function NavigationBar(): FunctionComponent {
|
||||
const { location, navigate } = useNavigation()
|
||||
const { location, navigate } = useNavigation();
|
||||
|
||||
const classes = useStyles()
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar position="fixed">
|
||||
<Toolbar>
|
||||
<Typography
|
||||
edge="start"
|
||||
variant="h6"
|
||||
className={classes.title}
|
||||
>
|
||||
{routePrettyNames[location]}
|
||||
</Typography>
|
||||
<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} />
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<AppBar position="fixed">
|
||||
<Toolbar>
|
||||
<Typography edge="start" variant="h6" className={classes.title}>
|
||||
{routePrettyNames[location]}
|
||||
</Typography>
|
||||
<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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,79 +1,77 @@
|
|||
import Box from '@material-ui/core/Box'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Card from '@material-ui/core/Card'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'
|
||||
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useState } from 'preact/hooks'
|
||||
import Box from "@material-ui/core/Box";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline";
|
||||
import ErrorOutlineIcon from "@material-ui/icons/ErrorOutline";
|
||||
import { FunctionComponent } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import useSettings from './hooks/useSettings'
|
||||
import useSettings from "./hooks/useSettings";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
urlCard: {
|
||||
margin: '5px',
|
||||
padding: '5px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
},
|
||||
})
|
||||
urlCard: {
|
||||
margin: "5px",
|
||||
padding: "5px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
},
|
||||
});
|
||||
|
||||
function isValidUrl(url: string): boolean {
|
||||
const urlPattern = /(https?:\/\/)?(www\.)?[\w.-_]+\.[a-zA-Z]{2,3}/
|
||||
const urlPattern = /(https?:\/\/)?(www\.)?[\w.-_]+\.[a-zA-Z]{2,3}/;
|
||||
|
||||
return urlPattern.test(url)
|
||||
return urlPattern.test(url);
|
||||
}
|
||||
|
||||
export default function SettingsPanel(): FunctionComponent {
|
||||
const { getSettings, setSettings } = useSettings()
|
||||
const settings = getSettings()
|
||||
const [feedUrlsForm, setFeedUrlsForm] = useState(settings.feedUrls)
|
||||
const { getSettings, setSettings } = useSettings();
|
||||
const settings = getSettings();
|
||||
const [feedUrlsForm, setFeedUrlsForm] = useState(settings.feedUrls);
|
||||
|
||||
const classes = useStyles()
|
||||
const urlCards = feedUrlsForm.map((url) => (
|
||||
<Card key={`url_${url}`} variant="outlined" className={classes.urlCard}>
|
||||
{isValidUrl(url) ? (
|
||||
<CheckCircleOutlineIcon color="primary" />
|
||||
) : (
|
||||
<ErrorOutlineIcon color="error" />
|
||||
)}{' '}
|
||||
{url}
|
||||
</Card>
|
||||
))
|
||||
const classes = useStyles();
|
||||
const urlCards = feedUrlsForm.map((url) => (
|
||||
<Card key={`url_${url}`} variant="outlined" className={classes.urlCard}>
|
||||
{isValidUrl(url) ? (
|
||||
<CheckCircleOutlineIcon color="primary" />
|
||||
) : (
|
||||
<ErrorOutlineIcon color="error" />
|
||||
)}{" "}
|
||||
{url}
|
||||
</Card>
|
||||
));
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Typography paragraph>
|
||||
{
|
||||
'Enter URLs to fetch feeds from. Invalid URLs are discarded on save.'
|
||||
}
|
||||
</Typography>
|
||||
<TextField
|
||||
multiline
|
||||
rows={5}
|
||||
variant="outlined"
|
||||
label="Feed URLs"
|
||||
fullWidth
|
||||
value={feedUrlsForm.join('\n')}
|
||||
onChange={(v) => setFeedUrlsForm(v.target.value.split('\n'))}
|
||||
/>
|
||||
<Box display="flex" flexDirection="column">
|
||||
{urlCards}
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
const validUrls = feedUrlsForm.filter(isValidUrl)
|
||||
setSettings<string[]>('feedUrls', validUrls)
|
||||
setFeedUrlsForm(validUrls)
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
return (
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Typography paragraph>
|
||||
{"Enter URLs to fetch feeds from. Invalid URLs are discarded on save."}
|
||||
</Typography>
|
||||
<TextField
|
||||
multiline
|
||||
rows={5}
|
||||
variant="outlined"
|
||||
label="Feed URLs"
|
||||
fullWidth
|
||||
value={feedUrlsForm.join("\n")}
|
||||
onChange={(v) => setFeedUrlsForm(v.target.value.split("\n"))}
|
||||
/>
|
||||
<Box display="flex" flexDirection="column">
|
||||
{urlCards}
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
const validUrls = feedUrlsForm.filter(isValidUrl);
|
||||
setSettings<string[]>("feedUrls", validUrls);
|
||||
setFeedUrlsForm(validUrls);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
export const panelIdentifiers = {
|
||||
FEEDS: 'feeds',
|
||||
SETTINGS: 'settings',
|
||||
}
|
||||
FEEDS: "feeds",
|
||||
SETTINGS: "settings",
|
||||
};
|
||||
|
||||
export const readablePanelIdentifiers = {
|
||||
[panelIdentifiers.FEEDS]: 'Feeds',
|
||||
[panelIdentifiers.SETTINGS]: 'Settings',
|
||||
}
|
||||
[panelIdentifiers.FEEDS]: "Feeds",
|
||||
[panelIdentifiers.SETTINGS]: "Settings",
|
||||
};
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
interface LocalStorageHookReturnType {
|
||||
getValue: <T>(k: string) => T
|
||||
setValue: <T>(k: string, v: T) => void
|
||||
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 },
|
||||
{ isJSON }: { isJSON: boolean } = { isJSON: false },
|
||||
): LocalStorageHookReturnType {
|
||||
if (!window.localStorage) throw new Error('Local Storage is not available')
|
||||
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,
|
||||
)
|
||||
},
|
||||
}
|
||||
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,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,54 +1,53 @@
|
|||
import { ComponentChildren, FunctionComponent, createContext } from 'preact'
|
||||
import { useCallback, useContext, useState } from 'preact/hooks'
|
||||
import { ComponentChildren, FunctionComponent, createContext } from "preact";
|
||||
import { useCallback, useContext, useState } from "preact/hooks";
|
||||
|
||||
interface INavigationContext {
|
||||
location: string
|
||||
navigate: (url: string) => void
|
||||
location: string;
|
||||
navigate: (url: string) => void;
|
||||
}
|
||||
|
||||
const NavigationContext = createContext(null)
|
||||
const NavigationContext = createContext(null);
|
||||
|
||||
export default function useNavigation(): INavigationContext {
|
||||
const context = useContext(NavigationContext)
|
||||
const context = useContext(NavigationContext);
|
||||
|
||||
if (!context) throw Error('Invalid navigation context')
|
||||
if (!context) throw Error("Invalid navigation context");
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
export const routes = {
|
||||
FEEDS: '/feeds/',
|
||||
SETTINGS: '/settings/',
|
||||
}
|
||||
FEEDS: "/feeds/",
|
||||
SETTINGS: "/settings/",
|
||||
};
|
||||
|
||||
export function NavigationProvider({
|
||||
children,
|
||||
children,
|
||||
}: {
|
||||
children: ComponentChildren
|
||||
children: ComponentChildren;
|
||||
}): FunctionComponent<{ children: ComponentChildren }> {
|
||||
const [location, setLocation] = useState<string>(window.location.pathname)
|
||||
const [location, setLocation] = useState<string>(window.location.pathname);
|
||||
|
||||
const navigate = useCallback(
|
||||
(url: string) => {
|
||||
window.history.pushState({}, '', url)
|
||||
setLocation(url)
|
||||
},
|
||||
[setLocation],
|
||||
)
|
||||
const navigate = useCallback(
|
||||
(url: string) => {
|
||||
window.history.pushState({}, "", url);
|
||||
setLocation(url);
|
||||
},
|
||||
[setLocation],
|
||||
);
|
||||
|
||||
const suffixedLocation = location.endsWith('/') ? location : `${location}/`
|
||||
const suffixedLocation = location.endsWith("/") ? location : `${location}/`;
|
||||
|
||||
if (suffixedLocation !== location) {
|
||||
window.history.replaceState({}, '', suffixedLocation)
|
||||
setLocation(suffixedLocation)
|
||||
}
|
||||
if (suffixedLocation !== location) {
|
||||
window.history.replaceState({}, "", suffixedLocation);
|
||||
setLocation(suffixedLocation);
|
||||
}
|
||||
|
||||
if (!Object.values(routes).includes(suffixedLocation))
|
||||
navigate(routes.FEEDS)
|
||||
if (!Object.values(routes).includes(suffixedLocation)) navigate(routes.FEEDS);
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider value={{ location, navigate }}>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
)
|
||||
return (
|
||||
<NavigationContext.Provider value={{ location, navigate }}>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,100 +1,98 @@
|
|||
import { parseFeed } from 'htmlparser2'
|
||||
import { useQueries } from 'react-query'
|
||||
import { parseFeed } from "htmlparser2";
|
||||
import { useQueries } from "react-query";
|
||||
|
||||
import { Feed } from '../types'
|
||||
import { isDev } from '../utils'
|
||||
import { Feed } from "../types";
|
||||
import { isDev } from "../utils";
|
||||
|
||||
import useLocalStorage from './useLocalStorage'
|
||||
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)
|
||||
}
|
||||
// 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,
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}, []),
|
||||
}
|
||||
return items;
|
||||
}, []),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchFeed(
|
||||
url: string,
|
||||
persistedData: Feed | null,
|
||||
url: string,
|
||||
persistedData: Feed | null,
|
||||
): Promise<Feed> {
|
||||
const response = await fetch(`/.netlify/functions/rss-proxy?url=${url}`)
|
||||
const response = await fetch(`/.netlify/functions/rss-proxy?url=${url}`);
|
||||
|
||||
const responseData = await response.text()
|
||||
const responseData = await response.text();
|
||||
|
||||
try {
|
||||
const newFeedData = parseFeed(responseData)
|
||||
const newFeed = processFeedXML(newFeedData)
|
||||
const mergedFeeds = persistedData
|
||||
? mergeFeeds(persistedData, newFeed)
|
||||
: newFeed
|
||||
try {
|
||||
const newFeedData = parseFeed(responseData);
|
||||
const newFeed = processFeedXML(newFeedData);
|
||||
const mergedFeeds = persistedData
|
||||
? mergeFeeds(persistedData, newFeed)
|
||||
: newFeed;
|
||||
|
||||
return mergedFeeds
|
||||
} catch (e) {
|
||||
if (isDev()) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
return mergedFeeds;
|
||||
} catch (e) {
|
||||
if (isDev()) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return persistedData
|
||||
return persistedData;
|
||||
}
|
||||
|
||||
export default function useRSSFeeds(urls: string[]): { feeds: Feed[] } {
|
||||
const { getValue, setValue } = useLocalStorage({ isJSON: true })
|
||||
const { getValue, setValue } = useLocalStorage({ isJSON: true });
|
||||
|
||||
const queries = useQueries(
|
||||
urls.map((feedUrl: string) => {
|
||||
const localStorageKey = `feed_${feedUrl}`
|
||||
const persistedData = getValue<Feed>(localStorageKey)
|
||||
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,
|
||||
}
|
||||
}),
|
||||
)
|
||||
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 && current.data) fetchedFeeds.push(current.data)
|
||||
const fetchedFeeds = queries.reduce((fetchedFeeds: Feed[], current) => {
|
||||
if (current.isSuccess && current.data) fetchedFeeds.push(current.data);
|
||||
|
||||
return fetchedFeeds
|
||||
}, [])
|
||||
return fetchedFeeds;
|
||||
}, []);
|
||||
|
||||
return { feeds: fetchedFeeds }
|
||||
return { feeds: fetchedFeeds };
|
||||
}
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import { Settings } from '../types'
|
||||
import { Settings } from "../types";
|
||||
|
||||
import useLocalStorage from './useLocalStorage'
|
||||
import useLocalStorage from "./useLocalStorage";
|
||||
|
||||
const defaultSettings = {
|
||||
feedUrls: [],
|
||||
}
|
||||
feedUrls: [],
|
||||
};
|
||||
|
||||
export default function useSettings(): {
|
||||
getSettings: () => Settings
|
||||
setSettings: <T>(k: string, value: T) => void
|
||||
getSettings: () => Settings;
|
||||
setSettings: <T>(k: string, value: T) => void;
|
||||
} {
|
||||
const { getValue, setValue } = useLocalStorage({ isJSON: true })
|
||||
const { getValue, setValue } = useLocalStorage({ isJSON: true });
|
||||
|
||||
const getSettings = (): Settings =>
|
||||
getValue<Settings>('settings') ?? defaultSettings
|
||||
const getSettings = (): Settings =>
|
||||
getValue<Settings>("settings") ?? defaultSettings;
|
||||
|
||||
const setSettings = <T>(key: string, value: T) => {
|
||||
const current = getSettings()
|
||||
const setSettings = <T>(key: string, value: T) => {
|
||||
const current = getSettings();
|
||||
|
||||
setValue<Settings>('settings', { ...current, [key]: value })
|
||||
}
|
||||
setValue<Settings>("settings", { ...current, [key]: value });
|
||||
};
|
||||
|
||||
return { getSettings, setSettings }
|
||||
return { getSettings, setSettings };
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
if (process.env.NODE_ENV === 'development') {
|
||||
require('preact/debug')
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
require("preact/debug");
|
||||
}
|
||||
|
||||
import { render } from 'preact'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
import { render } from "preact";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
|
||||
import App from './App'
|
||||
import { NavigationProvider } from './hooks/useNavigation'
|
||||
import App from "./App";
|
||||
import { NavigationProvider } from "./hooks/useNavigation";
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
render(
|
||||
<NavigationProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</NavigationProvider>,
|
||||
document.getElementById('app'),
|
||||
)
|
||||
<NavigationProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</NavigationProvider>,
|
||||
document.getElementById("app"),
|
||||
);
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
export interface Feed {
|
||||
title: string
|
||||
lastPull: string
|
||||
items: Item[]
|
||||
title: string;
|
||||
lastPull: string;
|
||||
items: Item[];
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
title: string
|
||||
url: string
|
||||
published: Date
|
||||
source: string
|
||||
title: string;
|
||||
url: string;
|
||||
published: Date;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
loaded: boolean
|
||||
rssItems: Item[]
|
||||
feedUrls: string[]
|
||||
activePanel: string
|
||||
loaded: boolean;
|
||||
rssItems: Item[];
|
||||
feedUrls: string[];
|
||||
activePanel: string;
|
||||
}
|
||||
|
||||
export interface RSSData {
|
||||
items: Item[]
|
||||
lastPushed: Date
|
||||
items: Item[];
|
||||
lastPushed: Date;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
feedUrls: string[]
|
||||
feedUrls: string[];
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import type { Feed, Item } from './types'
|
||||
import type { Feed, Item } from "./types";
|
||||
|
||||
export default function sortFeedItemsByDate(feeds: Feed[]): Item[] {
|
||||
const flattened = feeds.reduce((flattenedFeeds, feed) => {
|
||||
const items = feed.items.map((item) => ({
|
||||
...item,
|
||||
feedTitle: feed.title,
|
||||
}))
|
||||
flattenedFeeds.push(...items)
|
||||
return flattenedFeeds
|
||||
}, [])
|
||||
const flattened = feeds.reduce((flattenedFeeds, feed) => {
|
||||
const items = feed.items.map((item) => ({
|
||||
...item,
|
||||
feedTitle: feed.title,
|
||||
}));
|
||||
flattenedFeeds.push(...items);
|
||||
return flattenedFeeds;
|
||||
}, []);
|
||||
|
||||
return flattened.sort((first, second) =>
|
||||
first.published > second.published ? -1 : 1,
|
||||
)
|
||||
return flattened.sort((first, second) =>
|
||||
first.published > second.published ? -1 : 1,
|
||||
);
|
||||
}
|
||||
|
||||
export function isDev(): boolean {
|
||||
return process.env.NODE_ENV === 'development'
|
||||
return process.env.NODE_ENV === "development";
|
||||
}
|
||||
|
|
12
rome.json
Normal file
12
rome.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"$schema": "https://docs.rome.tools/schemas/12.1.3/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": false
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue