build: switch linterformatter to rome (#120)

* build: replace eslint with rome

* chore: run lint+formatter

* chore: more cleanup
This commit is contained in:
Marc 2023-07-01 00:26:28 -04:00 committed by GitHub
parent fc531cd84a
commit 19cb3dba19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 538 additions and 3044 deletions

View file

@ -1,6 +0,0 @@
module.exports = {
extends: '@tophat',
rules: {
"react/react-in-jsx-scope": "off"
}
}

View file

@ -10,8 +10,8 @@
"scripts": { "scripts": {
"start:app": "yarn workspace app start", "start:app": "yarn workspace app start",
"start:api": "yarn workspace api start", "start:api": "yarn workspace api start",
"lint": "eslint packages/**/*.ts", "lint": "yarn rome format packages/**/src/*.ts && yarn rome check packages/**/src/*.ts",
"lint:fix": "eslint packages/**/*.ts netlify --fix", "lint:fix": "yarn rome format packages/**/src/*.ts --write && yarn rome check packages/**/src/*.ts --apply",
"types": "tsc --noEmit", "types": "tsc --noEmit",
"clean": "rm -rf dist/*", "clean": "rm -rf dist/*",
"build:app": "yarn workspace app build", "build:app": "yarn workspace app build",
@ -22,25 +22,12 @@
"devDependencies": { "devDependencies": {
"@parcel/reporter-bundle-analyzer": "^2.9.3", "@parcel/reporter-bundle-analyzer": "^2.9.3",
"@parcel/validator-typescript": "^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", "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", "jest": "29.3.1",
"netlify-cli": "^15.7.0", "netlify-cli": "^15.7.0",
"parcel": "^2.9.3", "parcel": "^2.9.3",
"prettier": "2.7.1", "prettier": "2.7.1",
"rome": "^12.1.3",
"typescript": "^4.9.4" "typescript": "^4.9.4"
}, },
"resolutions": { "resolutions": {

View file

@ -1,14 +1,14 @@
import { import {
type Express, type Express,
type Request, type Request,
type Response, type Response,
default as express, default as express,
} from 'express' } from "express";
const app: Express = express() const app: Express = express();
app.get('/', (req: Request, res: Response) => { app.get("/", (req: Request, res: Response) => {
res.send('ok') res.send("ok");
}) });
app.listen(8081, () => {}) app.listen(8081, () => {});

View file

@ -1,18 +1,18 @@
import { FunctionComponent } from 'preact' import { FunctionComponent } from "preact";
import FeedsPanel from './FeedsPanel' import FeedsPanel from "./FeedsPanel";
import NavigationBar from './NavigationBar' import NavigationBar from "./NavigationBar";
import SettingsPanel from './SettingsPanel' import SettingsPanel from "./SettingsPanel";
import useNavigation, { routes } from './hooks/useNavigation' import useNavigation, { routes } from "./hooks/useNavigation";
export default function App(): FunctionComponent { export default function App(): FunctionComponent {
const { location } = useNavigation() const { location } = useNavigation();
return ( return (
<> <>
<NavigationBar /> <NavigationBar />
{location === routes.FEEDS ? <FeedsPanel /> : null} {location === routes.FEEDS ? <FeedsPanel /> : null}
{location === routes.SETTINGS ? <SettingsPanel /> : null} {location === routes.SETTINGS ? <SettingsPanel /> : null}
</> </>
) );
} }

View file

@ -1,76 +1,75 @@
import Box from '@material-ui/core/Box' import Box from "@material-ui/core/Box";
import Card from '@material-ui/core/Card' import Card from "@material-ui/core/Card";
import Typography from '@material-ui/core/Typography' import Typography from "@material-ui/core/Typography";
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from "@material-ui/core/styles";
import { FunctionComponent } from 'preact' import { FunctionComponent } from "preact";
import useRSSFeeds from './hooks/useRSSFeeds' import useRSSFeeds from "./hooks/useRSSFeeds";
import useSettings from './hooks/useSettings' import useSettings from "./hooks/useSettings";
import sortFeedItemsByDate from './utils' import sortFeedItemsByDate from "./utils";
interface CardProps { interface CardProps {
title: string title: string;
url: string url: string;
published: Date published: Date;
feedTitle: string feedTitle: string;
} }
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
margin: '5px', margin: "5px",
padding: '10px', padding: "10px",
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
}, },
}) });
function ItemCard(props: CardProps) { function ItemCard(props: CardProps) {
const { title, url, published, feedTitle } = props const { title, url, published, feedTitle } = props;
const classes = useStyles() const classes = useStyles();
const formattedDate = new Date(published).toLocaleString('en-GB', { const formattedDate = new Date(published).toLocaleString("en-GB", {
timeZone: 'UTC', timeZone: "UTC",
}) });
return ( return (
<Card className={classes.root}> <Card className={classes.root}>
<a href={url}>{title}</a> <a href={url}>{title}</a>
<span>{`${feedTitle} - ${formattedDate}`}</span> <span>{`${feedTitle} - ${formattedDate}`}</span>
</Card> </Card>
) );
} }
function NoItemsNotice() { function NoItemsNotice() {
return ( return (
<Box <Box
alignSelf="center" alignSelf="center"
display="flex" display="flex"
flexDirection="column" flexDirection="column"
alignItems="center" alignItems="center"
mt="50px" mt="50px"
> >
<Typography variant="h6">Nothing to see here!</Typography> <Typography variant="h6">Nothing to see here!</Typography>
<Typography> <Typography>
Add some feeds in the <strong>Settings</strong> panel to get Add some feeds in the <strong>Settings</strong> panel to get started!
started! </Typography>
</Typography> </Box>
</Box> );
)
} }
export default function FeedsPanel(): FunctionComponent { export default function FeedsPanel(): FunctionComponent {
const { getSettings } = useSettings() const { getSettings } = useSettings();
const settings = getSettings() const settings = getSettings();
const { feeds } = useRSSFeeds(settings.feedUrls) const { feeds } = useRSSFeeds(settings.feedUrls);
const flattenedItems = sortFeedItemsByDate(feeds) const flattenedItems = sortFeedItemsByDate(feeds);
const cardList = flattenedItems.map((item) => ( const cardList = flattenedItems.map((item) => (
<ItemCard {...item} key={`feed_item_${item.title.replace(' ', '_')}`} /> <ItemCard {...item} key={`feed_item_${item.title.replace(" ", "_")}`} />
)) ));
return ( return (
<Box display="flex" flexDirection="column"> <Box display="flex" flexDirection="column">
{cardList.length > 0 ? cardList : <NoItemsNotice />} {cardList.length > 0 ? cardList : <NoItemsNotice />}
</Box> </Box>
) );
} }

View file

@ -1,55 +1,51 @@
import AppBar from '@material-ui/core/AppBar' import AppBar from "@material-ui/core/AppBar";
import Button from '@material-ui/core/Button' import Button from "@material-ui/core/Button";
import Toolbar from '@material-ui/core/Toolbar' import Toolbar from "@material-ui/core/Toolbar";
import Typography from '@material-ui/core/Typography' import Typography from "@material-ui/core/Typography";
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from "@material-ui/core/styles";
import { FunctionComponent } from 'preact' import { FunctionComponent } from "preact";
import useNavigation, { routes } from './hooks/useNavigation' import useNavigation, { routes } from "./hooks/useNavigation";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
offset: theme.mixins.toolbar, offset: theme.mixins.toolbar,
title: { flexGrow: 1 }, title: { flexGrow: 1 },
})) }));
const routePrettyNames = { const routePrettyNames = {
[routes.SETTINGS]: 'Settings', [routes.SETTINGS]: "Settings",
[routes.FEEDS]: 'Your feeds', [routes.FEEDS]: "Your feeds",
} };
export default function NavigationBar(): FunctionComponent { export default function NavigationBar(): FunctionComponent {
const { location, navigate } = useNavigation() const { location, navigate } = useNavigation();
const classes = useStyles() const classes = useStyles();
return ( return (
<> <>
<AppBar position="fixed"> <AppBar position="fixed">
<Toolbar> <Toolbar>
<Typography <Typography edge="start" variant="h6" className={classes.title}>
edge="start" {routePrettyNames[location]}
variant="h6" </Typography>
className={classes.title} <Button
> key={"navigation-feeds"}
{routePrettyNames[location]} color="inherit"
</Typography> onClick={() => navigate(routes.FEEDS)}
<Button >
key={'navigation-feeds'} {routePrettyNames[routes.FEEDS]}
color="inherit" </Button>
onClick={() => navigate(routes.FEEDS)} <Button
> key={"navigation-settings"}
{routePrettyNames[routes.FEEDS]} color="inherit"
</Button> onClick={() => navigate(routes.SETTINGS)}
<Button >
key={'navigation-settings'} {routePrettyNames[routes.SETTINGS]}
color="inherit" </Button>
onClick={() => navigate(routes.SETTINGS)} </Toolbar>
> </AppBar>
{routePrettyNames[routes.SETTINGS]} <div className={classes.offset} />
</Button> </>
</Toolbar> );
</AppBar>
<div className={classes.offset} />
</>
)
} }

View file

@ -1,79 +1,77 @@
import Box from '@material-ui/core/Box' import Box from "@material-ui/core/Box";
import Button from '@material-ui/core/Button' import Button from "@material-ui/core/Button";
import Card from '@material-ui/core/Card' import Card from "@material-ui/core/Card";
import TextField from '@material-ui/core/TextField' import TextField from "@material-ui/core/TextField";
import Typography from '@material-ui/core/Typography' import Typography from "@material-ui/core/Typography";
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from "@material-ui/core/styles";
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 { FunctionComponent } from 'preact' import { FunctionComponent } from "preact";
import { useState } from 'preact/hooks' import { useState } from "preact/hooks";
import useSettings from './hooks/useSettings' import useSettings from "./hooks/useSettings";
const useStyles = makeStyles({ const useStyles = makeStyles({
urlCard: { urlCard: {
margin: '5px', margin: "5px",
padding: '5px', padding: "5px",
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
gap: '5px', gap: "5px",
}, },
}) });
function isValidUrl(url: string): boolean { 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 { export default function SettingsPanel(): FunctionComponent {
const { getSettings, setSettings } = useSettings() const { getSettings, setSettings } = useSettings();
const settings = getSettings() const settings = getSettings();
const [feedUrlsForm, setFeedUrlsForm] = useState(settings.feedUrls) const [feedUrlsForm, setFeedUrlsForm] = useState(settings.feedUrls);
const classes = useStyles() const classes = useStyles();
const urlCards = feedUrlsForm.map((url) => ( const urlCards = feedUrlsForm.map((url) => (
<Card key={`url_${url}`} variant="outlined" className={classes.urlCard}> <Card key={`url_${url}`} variant="outlined" className={classes.urlCard}>
{isValidUrl(url) ? ( {isValidUrl(url) ? (
<CheckCircleOutlineIcon color="primary" /> <CheckCircleOutlineIcon color="primary" />
) : ( ) : (
<ErrorOutlineIcon color="error" /> <ErrorOutlineIcon color="error" />
)}{' '} )}{" "}
{url} {url}
</Card> </Card>
)) ));
return ( return (
<Box display="flex" flexDirection="column"> <Box display="flex" flexDirection="column">
<Typography paragraph> <Typography paragraph>
{ {"Enter URLs to fetch feeds from. Invalid URLs are discarded on save."}
'Enter URLs to fetch feeds from. Invalid URLs are discarded on save.' </Typography>
} <TextField
</Typography> multiline
<TextField rows={5}
multiline variant="outlined"
rows={5} label="Feed URLs"
variant="outlined" fullWidth
label="Feed URLs" value={feedUrlsForm.join("\n")}
fullWidth onChange={(v) => setFeedUrlsForm(v.target.value.split("\n"))}
value={feedUrlsForm.join('\n')} />
onChange={(v) => setFeedUrlsForm(v.target.value.split('\n'))} <Box display="flex" flexDirection="column">
/> {urlCards}
<Box display="flex" flexDirection="column"> </Box>
{urlCards} <Button
</Box> variant="contained"
<Button color="primary"
variant="contained" onClick={() => {
color="primary" const validUrls = feedUrlsForm.filter(isValidUrl);
onClick={() => { setSettings<string[]>("feedUrls", validUrls);
const validUrls = feedUrlsForm.filter(isValidUrl) setFeedUrlsForm(validUrls);
setSettings<string[]>('feedUrls', validUrls) }}
setFeedUrlsForm(validUrls) >
}} Save
> </Button>
Save </Box>
</Button> );
</Box>
)
} }

View file

@ -1,9 +1,9 @@
export const panelIdentifiers = { export const panelIdentifiers = {
FEEDS: 'feeds', FEEDS: "feeds",
SETTINGS: 'settings', SETTINGS: "settings",
} };
export const readablePanelIdentifiers = { export const readablePanelIdentifiers = {
[panelIdentifiers.FEEDS]: 'Feeds', [panelIdentifiers.FEEDS]: "Feeds",
[panelIdentifiers.SETTINGS]: 'Settings', [panelIdentifiers.SETTINGS]: "Settings",
} };

View file

@ -1,26 +1,26 @@
interface LocalStorageHookReturnType { interface LocalStorageHookReturnType {
getValue: <T>(k: string) => T getValue: <T>(k: string) => T;
setValue: <T>(k: string, v: T) => void setValue: <T>(k: string, v: T) => void;
} }
/* /*
* Encapsulates Local Storage interactions. * Encapsulates Local Storage interactions.
*/ */
export default function useLocalStorage( export default function useLocalStorage(
{ isJSON }: { isJSON: boolean } = { isJSON: false }, { isJSON }: { isJSON: boolean } = { isJSON: false },
): LocalStorageHookReturnType { ): LocalStorageHookReturnType {
if (!window.localStorage) throw new Error('Local Storage is not available') if (!window.localStorage) throw new Error("Local Storage is not available");
return { return {
getValue: <T>(key: string): T => { getValue: <T>(key: string): T => {
const raw = window.localStorage.getItem(key) const raw = window.localStorage.getItem(key);
return (isJSON ? JSON.parse(raw) : raw) as T return (isJSON ? JSON.parse(raw) : raw) as T;
}, },
setValue: <T>(key: string, value: T) => { setValue: <T>(key: string, value: T) => {
window.localStorage.setItem( window.localStorage.setItem(
key, key,
(isJSON ? JSON.stringify(value) : value) as string, (isJSON ? JSON.stringify(value) : value) as string,
) );
}, },
} };
} }

View file

@ -1,54 +1,53 @@
import { ComponentChildren, FunctionComponent, createContext } from 'preact' import { ComponentChildren, FunctionComponent, createContext } from "preact";
import { useCallback, useContext, useState } from 'preact/hooks' import { useCallback, useContext, useState } from "preact/hooks";
interface INavigationContext { interface INavigationContext {
location: string location: string;
navigate: (url: string) => void navigate: (url: string) => void;
} }
const NavigationContext = createContext(null) const NavigationContext = createContext(null);
export default function useNavigation(): INavigationContext { 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 = { export const routes = {
FEEDS: '/feeds/', FEEDS: "/feeds/",
SETTINGS: '/settings/', SETTINGS: "/settings/",
} };
export function NavigationProvider({ export function NavigationProvider({
children, children,
}: { }: {
children: ComponentChildren children: ComponentChildren;
}): FunctionComponent<{ children: ComponentChildren }> { }): FunctionComponent<{ children: ComponentChildren }> {
const [location, setLocation] = useState<string>(window.location.pathname) const [location, setLocation] = useState<string>(window.location.pathname);
const navigate = useCallback( const navigate = useCallback(
(url: string) => { (url: string) => {
window.history.pushState({}, '', url) window.history.pushState({}, "", url);
setLocation(url) setLocation(url);
}, },
[setLocation], [setLocation],
) );
const suffixedLocation = location.endsWith('/') ? location : `${location}/` const suffixedLocation = location.endsWith("/") ? location : `${location}/`;
if (suffixedLocation !== location) { if (suffixedLocation !== location) {
window.history.replaceState({}, '', suffixedLocation) window.history.replaceState({}, "", suffixedLocation);
setLocation(suffixedLocation) setLocation(suffixedLocation);
} }
if (!Object.values(routes).includes(suffixedLocation)) if (!Object.values(routes).includes(suffixedLocation)) navigate(routes.FEEDS);
navigate(routes.FEEDS)
return ( return (
<NavigationContext.Provider value={{ location, navigate }}> <NavigationContext.Provider value={{ location, navigate }}>
{children} {children}
</NavigationContext.Provider> </NavigationContext.Provider>
) );
} }

View file

@ -1,100 +1,98 @@
import { parseFeed } from 'htmlparser2' import { parseFeed } from "htmlparser2";
import { useQueries } from 'react-query' import { useQueries } from "react-query";
import { Feed } from '../types' import { Feed } from "../types";
import { isDev } from '../utils' import { isDev } from "../utils";
import useLocalStorage from './useLocalStorage' import useLocalStorage from "./useLocalStorage";
function mergeFeeds(first, second) { function mergeFeeds(first, second) {
// Assuming `second` is newer. // Assuming `second` is newer.
const seen = new Set(first.items.map((item) => item.url)) const seen = new Set(first.items.map((item) => item.url));
const mergedItems = second.items.reduce( const mergedItems = second.items.reduce(
(updatedItems, item) => { (updatedItems, item) => {
if (!seen.has(item.url)) { if (!seen.has(item.url)) {
updatedItems.push(item) updatedItems.push(item);
seen.add(item.url) seen.add(item.url);
} }
return updatedItems return updatedItems;
}, },
[...first.items], [...first.items],
) );
return { return {
...second, ...second,
items: mergedItems, items: mergedItems,
} };
} }
function processFeedXML(feed): Feed { function processFeedXML(feed): Feed {
return { return {
title: feed.title, title: feed.title,
lastPull: String(Date.now()), lastPull: String(Date.now()),
items: feed.items.reduce((items, feedItem) => { items: feed.items.reduce((items, feedItem) => {
items.push({ items.push({
title: feedItem.title, title: feedItem.title,
url: feedItem.link, url: feedItem.link,
published: new Date(feedItem.pubDate), published: new Date(feedItem.pubDate),
}) });
return items return items;
}, []), }, []),
} };
} }
async function fetchFeed( async function fetchFeed(
url: string, url: string,
persistedData: Feed | null, persistedData: Feed | null,
): Promise<Feed> { ): 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 { try {
const newFeedData = parseFeed(responseData) const newFeedData = parseFeed(responseData);
const newFeed = processFeedXML(newFeedData) const newFeed = processFeedXML(newFeedData);
const mergedFeeds = persistedData const mergedFeeds = persistedData
? mergeFeeds(persistedData, newFeed) ? mergeFeeds(persistedData, newFeed)
: newFeed : newFeed;
return mergedFeeds return mergedFeeds;
} catch (e) { } catch (e) {
if (isDev()) { if (isDev()) {
// eslint-disable-next-line no-console console.error(e);
console.error(e) }
} }
}
return persistedData return persistedData;
} }
export default function useRSSFeeds(urls: string[]): { feeds: Feed[] } { export default function useRSSFeeds(urls: string[]): { feeds: Feed[] } {
const { getValue, setValue } = useLocalStorage({ isJSON: true }) const { getValue, setValue } = useLocalStorage({ isJSON: true });
const queries = useQueries( const queries = useQueries(
urls.map((feedUrl: string) => { urls.map((feedUrl: string) => {
const localStorageKey = `feed_${feedUrl}` const localStorageKey = `feed_${feedUrl}`;
const persistedData = getValue<Feed>(localStorageKey) const persistedData = getValue<Feed>(localStorageKey);
return { return {
queryKey: ['feed', feedUrl], queryKey: ["feed", feedUrl],
queryFn: () => fetchFeed(feedUrl, persistedData), queryFn: () => fetchFeed(feedUrl, persistedData),
staleTime: 30000, staleTime: 30000,
onSuccess: (data: Feed) => { onSuccess: (data: Feed) => {
setValue(localStorageKey, data) setValue(localStorageKey, data);
}, },
initialData: persistedData ?? undefined, initialData: persistedData ?? undefined,
initialDataUpdatedAt: initialDataUpdatedAt: Number(persistedData?.lastPull) ?? undefined,
Number(persistedData?.lastPull) ?? undefined, };
} }),
}), );
)
const fetchedFeeds = queries.reduce((fetchedFeeds: Feed[], current) => { const fetchedFeeds = queries.reduce((fetchedFeeds: Feed[], current) => {
if (current.isSuccess && current.data) fetchedFeeds.push(current.data) if (current.isSuccess && current.data) fetchedFeeds.push(current.data);
return fetchedFeeds return fetchedFeeds;
}, []) }, []);
return { feeds: fetchedFeeds } return { feeds: fetchedFeeds };
} }

View file

@ -1,25 +1,25 @@
import { Settings } from '../types' import { Settings } from "../types";
import useLocalStorage from './useLocalStorage' import useLocalStorage from "./useLocalStorage";
const defaultSettings = { const defaultSettings = {
feedUrls: [], feedUrls: [],
} };
export default function useSettings(): { export default function useSettings(): {
getSettings: () => Settings getSettings: () => Settings;
setSettings: <T>(k: string, value: T) => void setSettings: <T>(k: string, value: T) => void;
} { } {
const { getValue, setValue } = useLocalStorage({ isJSON: true }) const { getValue, setValue } = useLocalStorage({ isJSON: true });
const getSettings = (): Settings => const getSettings = (): Settings =>
getValue<Settings>('settings') ?? defaultSettings getValue<Settings>("settings") ?? defaultSettings;
const setSettings = <T>(key: string, value: T) => { const setSettings = <T>(key: string, value: T) => {
const current = getSettings() const current = getSettings();
setValue<Settings>('settings', { ...current, [key]: value }) setValue<Settings>("settings", { ...current, [key]: value });
} };
return { getSettings, setSettings } return { getSettings, setSettings };
} }

View file

@ -1,20 +1,20 @@
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === "development") {
require('preact/debug') require("preact/debug");
} }
import { render } from 'preact' import { render } from "preact";
import { QueryClient, QueryClientProvider } from 'react-query' import { QueryClient, QueryClientProvider } from "react-query";
import App from './App' import App from "./App";
import { NavigationProvider } from './hooks/useNavigation' import { NavigationProvider } from "./hooks/useNavigation";
const queryClient = new QueryClient() const queryClient = new QueryClient();
render( render(
<NavigationProvider> <NavigationProvider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<App /> <App />
</QueryClientProvider> </QueryClientProvider>
</NavigationProvider>, </NavigationProvider>,
document.getElementById('app'), document.getElementById("app"),
) );

View file

@ -1,28 +1,28 @@
export interface Feed { export interface Feed {
title: string title: string;
lastPull: string lastPull: string;
items: Item[] items: Item[];
} }
export interface Item { export interface Item {
title: string title: string;
url: string url: string;
published: Date published: Date;
source: string source: string;
} }
export interface State { export interface State {
loaded: boolean loaded: boolean;
rssItems: Item[] rssItems: Item[];
feedUrls: string[] feedUrls: string[];
activePanel: string activePanel: string;
} }
export interface RSSData { export interface RSSData {
items: Item[] items: Item[];
lastPushed: Date lastPushed: Date;
} }
export interface Settings { export interface Settings {
feedUrls: string[] feedUrls: string[];
} }

View file

@ -1,20 +1,20 @@
import type { Feed, Item } from './types' import type { Feed, Item } from "./types";
export default function sortFeedItemsByDate(feeds: Feed[]): Item[] { export default function sortFeedItemsByDate(feeds: Feed[]): Item[] {
const flattened = feeds.reduce((flattenedFeeds, feed) => { const flattened = feeds.reduce((flattenedFeeds, feed) => {
const items = feed.items.map((item) => ({ const items = feed.items.map((item) => ({
...item, ...item,
feedTitle: feed.title, feedTitle: feed.title,
})) }));
flattenedFeeds.push(...items) flattenedFeeds.push(...items);
return flattenedFeeds return flattenedFeeds;
}, []) }, []);
return flattened.sort((first, second) => return flattened.sort((first, second) =>
first.published > second.published ? -1 : 1, first.published > second.published ? -1 : 1,
) );
} }
export function isDev(): boolean { export function isDev(): boolean {
return process.env.NODE_ENV === 'development' return process.env.NODE_ENV === "development";
} }

12
rome.json Normal file
View 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
}
}
}

2803
yarn.lock

File diff suppressed because it is too large Load diff