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:
1138
package-lock.json
generated
1138
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
53
src/components/Watchlist/index.js
Normal file
53
src/components/Watchlist/index.js
Normal 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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
99
src/components/WatchlistTable/index.js
Normal file
99
src/components/WatchlistTable/index.js
Normal 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} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user