WAT-2: Just show items in a table with filtering

Use infinite loading to fill in all the pages in the background. Show
the keywords as pills/chips, but they aren't clickable (yet?).
This commit is contained in:
2024-06-24 01:37:58 -04:00
parent 5655e22075
commit b9b4a31f37
8 changed files with 1278 additions and 62 deletions

1138
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,9 +24,14 @@
},
"homepage": "https://github.com/averymd/public-plex-watchlist#readme",
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@mui/x-date-pickers": "^7.7.1",
"@tanstack/react-query": "^5.45.1",
"@tanstack/react-table": "^8.17.3",
"axios": "^1.7.2",
"material-react-table": "^2.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rss-parser": "^3.13.0"

View File

@@ -1,12 +1,12 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Wishlist from './components/Wishlist';
import Watchlist from './components/Watchlist';
export default function App() {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<Wishlist />
<Watchlist />
</QueryClientProvider>
);
}

View File

@@ -1,7 +1,17 @@
import axios from 'axios';
import Parser from 'rss-parser';
export async function fetchPlexWatchlistFeed(feed) {
let rssParser = new Parser();
return rssParser.parseURL(feed);
export async function fetchPlexWatchlistFeed({ pageParam }) {
let rssParser = new Parser({
customFields: {
item: [
'media:credit',
'media:thumbnail',
'media:keywords',
'media:rating',
],
},
});
let result = rssParser.parseURL(pageParam);
return result;
}

View File

@@ -0,0 +1,53 @@
import {
useInfiniteQuery,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { fetchPlexWatchlistFeed } from '../../api/plexApi';
import { useEffect, useMemo, useState } from 'react';
import React from 'react';
import WatchlistTable from '../WatchlistTable';
export default function Watchlist() {
const {
data,
error,
isPending,
isLoadingError,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['watchlistItems'],
queryFn: fetchPlexWatchlistFeed,
initialPageParam: process.env.BASE_RSS_FEED,
getNextPageParam: (lastPage, pages) => lastPage?.paginationLinks?.next,
});
useEffect(() => {
if (!isFetching && !isFetchingNextPage && hasNextPage) {
fetchNextPage();
}
}, [isFetching]);
const justItems = useMemo(
() => data?.pages?.flatMap((row) => row.items) ?? [],
[data]
);
return (
<>
<h1>Plex Watchlist</h1>
{!isPending && (
<WatchlistTable
items={justItems}
isLoadingItems={isFetching}
isErrorLoading={isLoadingError}
isPageLoading={isFetchingNextPage}
/>
)}
</>
);
}

View File

@@ -0,0 +1,99 @@
import { useMemo, useState } from 'react';
import {
MaterialReactTable,
useMaterialReactTable,
} from 'material-react-table';
import { Chip, Link, Stack } from '@mui/material';
import LaunchOutlined from '@mui/icons-material/Launch';
export default function WatchlistTable({
items,
isLoadingItems,
isErrorLoading,
isPageLoading,
}) {
const keywordOptions = useMemo(
() =>
[
...new Set(items.flatMap((item) => item['media:keywords'].split(', '))),
].map((keyword) => {
return { label: keyword, value: keyword };
}),
[]
);
const columns = useMemo(
() => [
{
header: 'Title',
accessorKey: 'title',
Cell: ({ renderedCellValue, row }) => (
<Link href={row.original.link} target="_blank">
{renderedCellValue} <LaunchOutlined fontSize="x-small" />
</Link>
),
size: 250,
},
{
header: 'Type',
id: 'category',
accessorFn: (row) => row.categories[0],
filterVariant: 'select',
size: 100,
},
{
header: 'Summary',
accessorKey: 'content',
minSize: 200,
size: 400,
maxSize: 600,
grow: false,
},
{
header: 'Keywords',
id: 'keywords',
accessorFn: (row) => {
return row['media:keywords'].split(', ');
},
Cell: ({ renderedCellValue, row }) => (
<Stack direction="row" spacing={0.25} useFlexGap flexWrap="wrap">
{renderedCellValue.map((keyword) => (
<Chip label={keyword} key={keyword} />
))}
</Stack>
),
minSize: 200,
size: 400,
maxSize: 600,
filterSelectOptions: keywordOptions,
},
{
header: 'Rating',
id: 'mediaRating',
accessorFn: (row) => row['media:rating']?.toUpperCase(),
filterVariant: 'multi-select',
},
],
[]
);
const table = useMaterialReactTable({
columns,
data: items,
layoutMode: 'grid-no-grow',
filterFromLeafRows: true,
enableFacetedValues: true,
state: {
isLoading: isLoadingItems,
showAlertBanner: isErrorLoading,
showProgressBars: isPageLoading,
},
initialState: {
showColumnFilters: true,
showGlobalFilter: true,
pagination: { pageSize: 100, pageIndex: 0 },
},
});
return <MaterialReactTable table={table} />;
}

View File

@@ -1,22 +0,0 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { fetchPlexWatchlistFeed } from '../../api/plexApi';
import { useState } from 'react';
export default function Wishlist() {
const [feedItems, setFeedItems] = useState([]);
const [currentFeedUrl, setCurrentFeedUrl] = useState(
process.env.BASE_RSS_FEED
);
const { isPending, isError, data, error, isFetching, isSuccess } = useQuery({
queryKey: ['wishlistItems', currentFeedUrl],
queryFn: () => fetchPlexWatchlistFeed(currentFeedUrl),
});
return (
<div>
<div>{isFetching ? 'Updating...' : ''}</div>
<div>{isSuccess ? data.title : 'nothing'}</div>
</div>
);
}

View File

@@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client';
// import './styles.css';
import App from './App';
import { useQueryClient } from '@tanstack/react-query';
const root = createRoot(document.getElementById('root'));