diff --git a/package-lock.json b/package-lock.json index a412726..f1e6d5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "axios": "^1.7.2", "globals": "^15.6.0", "material-react-table": "^2.13.0", - "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", "rss-parser": "^3.13.0" @@ -36,6 +35,7 @@ "nodemon": "^3.1.4", "parcel": "^2.12.0", "process": "^0.11.10", + "prop-types": "^15.8.1", "punycode": "^1.4.1", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", diff --git a/package.json b/package.json index 6d9471a..70ff83b 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "axios": "^1.7.2", "globals": "^15.6.0", "material-react-table": "^2.13.0", - "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", "rss-parser": "^3.13.0" @@ -52,6 +51,7 @@ "nodemon": "^3.1.4", "parcel": "^2.12.0", "process": "^0.11.10", + "prop-types": "^15.8.1", "punycode": "^1.4.1", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", diff --git a/src/components/Watchlist/index.js b/src/components/Watchlist/index.js index f807624..9ba827b 100644 --- a/src/components/Watchlist/index.js +++ b/src/components/Watchlist/index.js @@ -1,6 +1,6 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { fetchPlexWatchlistFeed } from '../../api/plexApi'; -import { useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import React from 'react'; import WatchlistTable from '../WatchlistTable'; @@ -8,6 +8,7 @@ export default function Watchlist() { const { data, isPending, + isLoading, isLoadingError, fetchNextPage, hasNextPage, @@ -18,15 +19,34 @@ export default function Watchlist() { queryFn: fetchPlexWatchlistFeed, initialPageParam: process.env.BASE_RSS_FEED, getNextPageParam: (lastPage) => lastPage?.paginationLinks?.next, + refetchOnWindowFocus: false, }); - useEffect(() => { - if (!isFetching && !isFetchingNextPage && hasNextPage) { - fetchNextPage(); - } - }, [isFetching, isFetchingNextPage, hasNextPage, fetchNextPage]); + let loadMoreItems = useCallback( + (containerRefElement) => { + // if (containerRefElement) { + // const { scrollHeight, scrollTop, clientHeight } = containerRefElement; + //once the user has scrolled within 400px of the bottom of the table, fetch more data if we can - const justItems = useMemo( + if ( + // scrollHeight - scrollTop - clientHeight < 400 && + !isFetching && + hasNextPage + ) { + fetchNextPage(); + } + // } + }, + [fetchNextPage, isFetching, hasNextPage] + ); + + useEffect(() => { + if (hasNextPage && !isFetchingNextPage) { + loadMoreItems(); + } + }, [loadMoreItems, hasNextPage, isFetchingNextPage, data]); + + const flatItems = useMemo( () => data?.pages?.flatMap((row) => row.items) ?? [], [data] ); @@ -35,10 +55,11 @@ export default function Watchlist() { <> {!isPending && ( )} diff --git a/src/components/WatchlistTable/index.js b/src/components/WatchlistTable/index.js index 7887730..5877c3f 100644 --- a/src/components/WatchlistTable/index.js +++ b/src/components/WatchlistTable/index.js @@ -1,10 +1,11 @@ -import { useMemo, useState } from 'react'; +import { useMemo, useRef, useEffect, useState, useCallback } from 'react'; import { MaterialReactTable, useMaterialReactTable, } from 'material-react-table'; import { Chip, Link, Stack } from '@mui/material'; import LaunchOutlined from '@mui/icons-material/Launch'; +import PropTypes from 'prop-types'; export default function WatchlistTable({ items, @@ -12,16 +13,42 @@ export default function WatchlistTable({ isErrorLoading, isPageLoading, }) { + const rowVirtualizerInstanceRef = useRef(null); + + const [columnFilters, setColumnFilters] = useState([]); + const [globalFilter, setGlobalFilter] = useState(); + const [sorting, setSorting] = useState([]); + const mediaKeywords = 'media:keywords'; + + //scroll to top of table when sorting or filters change + useEffect(() => { + try { + if (rowVirtualizerInstanceRef.current?.getTotalSize() > 0) { + rowVirtualizerInstanceRef.current?.scrollToIndex(0); + } + } catch (error) { + console.error(error); + } + }, [sorting, columnFilters, globalFilter]); + const keywordOptions = useMemo( () => - [ - ...new Set(items.flatMap((item) => item['media:keywords'].split(', '))), - ].map((keyword) => { - return { label: keyword, value: keyword }; - }), + [...new Set(items.flatMap((item) => item[mediaKeywords].split(', ')))] + .sort() + .map((keyword) => { + return { label: keyword, value: keyword }; + }), [items] ); + const getKeywordsForRow = useCallback((renderedCellValue, row) => { + if (typeof renderedCellValue === 'string') { + return renderedCellValue.split(', '); + } + + return row.original[mediaKeywords].split(', '); + }, []); + const columns = useMemo( () => [ { @@ -53,11 +80,11 @@ export default function WatchlistTable({ header: 'Keywords', id: 'keywords', accessorFn: (row) => { - return row['media:keywords'].split(', '); + return row[mediaKeywords].split(','); }, Cell: ({ renderedCellValue, row }) => ( - {renderedCellValue.map((keyword) => ( + {getKeywordsForRow(renderedCellValue, row).map((keyword) => ( ))} @@ -66,6 +93,14 @@ export default function WatchlistTable({ size: 400, maxSize: 700, filterSelectOptions: keywordOptions, + filterVariant: 'multi-select', + filterFn: (row, columnId, filterValue) => { + return ( + row.original[mediaKeywords] + .split(',') + .filter((keyword) => keyword.includes(filterValue)).length > 0 + ); + }, }, { header: 'Rating', @@ -74,24 +109,30 @@ export default function WatchlistTable({ filterVariant: 'multi-select', }, ], - [keywordOptions] + [keywordOptions, getKeywordsForRow] ); const table = useMaterialReactTable({ columns, data: items, layoutMode: 'semantic', - filterFromLeafRows: true, enableFacetedValues: true, enablePagination: false, enableRowVirtualization: true, + rowVirtualizerInstanceRef, + rowVirtualizerOptions: { overscan: 4 }, + onColumnFiltersChange: setColumnFilters, + onGlobalFilterChange: setGlobalFilter, + onSortingChange: setSorting, state: { isLoading: isLoadingItems, - showAlertBanner: isErrorLoading, showProgressBars: isPageLoading, + showAlertBanner: isErrorLoading, + columnFilters, + globalFilter, + sorting, }, initialState: { - showColumnFilters: true, showGlobalFilter: true, }, }); @@ -100,8 +141,9 @@ export default function WatchlistTable({ } WatchlistTable.propTypes = { - items: PropTypes.object.isRequired, + items: PropTypes.array.isRequired, isLoadingItems: PropTypes.bool.isRequired, isErrorLoading: PropTypes.bool.isRequired, isPageLoading: PropTypes.bool.isRequired, + loadMoreItems: PropTypes.func.isRequired, };