Merge branch 'release/1.0.0'
Some checks reported errors
Some checks reported errors
This commit is contained in:
1
.env.production
Normal file
1
.env.production
Normal file
@@ -0,0 +1 @@
|
||||
BASE_RSS_FEED=https://rss.plex.tv/d949de26-087e-4c7b-94d3-0f36b982ba21
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -128,3 +128,5 @@ dist
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
.vscode
|
||||
14
.pre-commit-config.yaml
Normal file
14
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
# .pre-commit-config.yaml
|
||||
repos:
|
||||
- repo: https://github.com/Yelp/detect-secrets
|
||||
rev: v1.5.0
|
||||
hooks:
|
||||
- id: detect-secrets
|
||||
args: ['--baseline', '.secrets.baseline']
|
||||
exclude: package.lock.json
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0 # Use the ref you want to point at
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: check-json
|
||||
#- id: check-yaml
|
||||
127
.secrets.baseline
Normal file
127
.secrets.baseline
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"version": "1.5.0",
|
||||
"plugins_used": [
|
||||
{
|
||||
"name": "ArtifactoryDetector"
|
||||
},
|
||||
{
|
||||
"name": "AWSKeyDetector"
|
||||
},
|
||||
{
|
||||
"name": "AzureStorageKeyDetector"
|
||||
},
|
||||
{
|
||||
"name": "Base64HighEntropyString",
|
||||
"limit": 4.5
|
||||
},
|
||||
{
|
||||
"name": "BasicAuthDetector"
|
||||
},
|
||||
{
|
||||
"name": "CloudantDetector"
|
||||
},
|
||||
{
|
||||
"name": "DiscordBotTokenDetector"
|
||||
},
|
||||
{
|
||||
"name": "GitHubTokenDetector"
|
||||
},
|
||||
{
|
||||
"name": "GitLabTokenDetector"
|
||||
},
|
||||
{
|
||||
"name": "HexHighEntropyString",
|
||||
"limit": 3.0
|
||||
},
|
||||
{
|
||||
"name": "IbmCloudIamDetector"
|
||||
},
|
||||
{
|
||||
"name": "IbmCosHmacDetector"
|
||||
},
|
||||
{
|
||||
"name": "IPPublicDetector"
|
||||
},
|
||||
{
|
||||
"name": "JwtTokenDetector"
|
||||
},
|
||||
{
|
||||
"name": "KeywordDetector",
|
||||
"keyword_exclude": ""
|
||||
},
|
||||
{
|
||||
"name": "MailchimpDetector"
|
||||
},
|
||||
{
|
||||
"name": "NpmDetector"
|
||||
},
|
||||
{
|
||||
"name": "OpenAIDetector"
|
||||
},
|
||||
{
|
||||
"name": "PrivateKeyDetector"
|
||||
},
|
||||
{
|
||||
"name": "PypiTokenDetector"
|
||||
},
|
||||
{
|
||||
"name": "SendGridDetector"
|
||||
},
|
||||
{
|
||||
"name": "SlackDetector"
|
||||
},
|
||||
{
|
||||
"name": "SoftlayerDetector"
|
||||
},
|
||||
{
|
||||
"name": "SquareOAuthDetector"
|
||||
},
|
||||
{
|
||||
"name": "StripeDetector"
|
||||
},
|
||||
{
|
||||
"name": "TelegramBotTokenDetector"
|
||||
},
|
||||
{
|
||||
"name": "TwilioKeyDetector"
|
||||
}
|
||||
],
|
||||
"filters_used": [
|
||||
{
|
||||
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
|
||||
},
|
||||
{
|
||||
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
|
||||
"min_level": 2
|
||||
},
|
||||
{
|
||||
"path": "detect_secrets.filters.heuristic.is_indirect_reference"
|
||||
},
|
||||
{
|
||||
"path": "detect_secrets.filters.heuristic.is_likely_id_string"
|
||||
},
|
||||
{
|
||||
"path": "detect_secrets.filters.heuristic.is_lock_file"
|
||||
},
|
||||
{
|
||||
"path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
|
||||
},
|
||||
{
|
||||
"path": "detect_secrets.filters.heuristic.is_potential_uuid"
|
||||
},
|
||||
{
|
||||
"path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
|
||||
},
|
||||
{
|
||||
"path": "detect_secrets.filters.heuristic.is_sequential_string"
|
||||
},
|
||||
{
|
||||
"path": "detect_secrets.filters.heuristic.is_swagger_file"
|
||||
},
|
||||
{
|
||||
"path": "detect_secrets.filters.heuristic.is_templated_secret"
|
||||
}
|
||||
],
|
||||
"results": {},
|
||||
"generated_at": "2024-06-28T03:54:46Z"
|
||||
}
|
||||
9
CHANGELOG.md
Normal file
9
CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.0
|
||||
|
||||
1. Initial release
|
||||
2. Load from RSS with automatic handling of paging in background load
|
||||
3. Searching and filtering, with faceted filters on keywords and rating
|
||||
|
||||
Doesn't include the keyword chips being clickable to add to the column filter.
|
||||
94
CodeAnalysisJenkinsfile
Normal file
94
CodeAnalysisJenkinsfile
Normal file
@@ -0,0 +1,94 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label 'python311 && amd64'
|
||||
}
|
||||
options {
|
||||
quietPeriod(120)
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
tools {nodejs "Node 20"}
|
||||
environment {
|
||||
DEPCHECK_SCAN_ACCOUNT = credentials('DEPCHECK_SCAN_ACCOUNT')
|
||||
DEPCHECK_CONNSTRING = credentials('DEPCHECK_CONNSTRING')
|
||||
SONATYPE_OSSINDEX_API_KEY = credentials('SONATYPE_OSSINDEX_API_KEY')
|
||||
SONAR_SCANNER_OPTS = '-Xmx768m'
|
||||
}
|
||||
stages {
|
||||
stage('Install Python Virtual Enviroment') {
|
||||
steps {
|
||||
sh 'echo $PATH'
|
||||
sh 'python3.11 -m venv env'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Install Application Dependencies') {
|
||||
steps {
|
||||
sh '''
|
||||
. env/bin/activate
|
||||
pip3.11 install --upgrade pip
|
||||
pip3.11 install -r requirements.txt
|
||||
corepack enable
|
||||
npm install
|
||||
mkdir reports
|
||||
deactivate
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('ESLint') {
|
||||
steps {
|
||||
script {
|
||||
try {
|
||||
sh ". env/bin/activate && npx eslint . -c eslint.config.mjs -o reports/eslint.json --format json || true"
|
||||
sh ". env/bin/activate && npx eslint . -c eslint.config.mjs -o reports/eslint-checkstyle.report --format checkstyle || true"
|
||||
}
|
||||
finally {
|
||||
recordIssues tool: esLint(pattern: '**/reports/eslint-checkstyle.report'), aggregatingResults: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('OWASP Dependency-Check Vulnerabilities') {
|
||||
steps {
|
||||
sh 'curl -O https://jdbc.postgresql.org/download/postgresql-42.7.3.jar'
|
||||
dependencyCheck odcInstallation: 'DepCheck',
|
||||
additionalArguments: '--project "Plex Watchlist" -o ./reports -f XML -f HTML -f JSON -f CSV --noupdate --connectionString $DEPCHECK_CONNSTRING --dbDriverPath postgresql-42.7.3.jar --dbDriverName org.postgresql.Driver --dbUser $DEPCHECK_SCAN_ACCOUNT_USR --dbPassword $DEPCHECK_SCAN_ACCOUNT_PSW --ossIndexUsername averymd@irrsinn.net --ossIndexPassword $SONATYPE_OSSINDEX_API_KEY'
|
||||
dependencyCheckPublisher pattern: 'reports/dependency-check-report.xml'
|
||||
}
|
||||
}
|
||||
|
||||
stage('SonarQube Analysis') {
|
||||
environment {
|
||||
scannerHome = tool 'SonarQubeDefault'
|
||||
}
|
||||
steps {
|
||||
withSonarQubeEnv('Personal SonarQube') {
|
||||
sh """
|
||||
. env/bin/activate
|
||||
${scannerHome}/bin/sonar-scanner \
|
||||
-Dsonar.dependencyCheck.jsonReportPath=reports/dependency-check-report.json \
|
||||
-Dsonar.dependencyCheck.xmlReportPath=reports/dependency-check-report.xml \
|
||||
-Dsonar.dependencyCheck.htmlReportPath=reports/dependency-check-report.html \
|
||||
-Dsonar.eslint.reportPaths=reports/eslint.json
|
||||
deactivate
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
cleanup {
|
||||
cleanWs()
|
||||
dir("${env.WORKSPACE}@tmp") {
|
||||
deleteDir()
|
||||
}
|
||||
dir("${env.WORKSPACE}@2") {
|
||||
deleteDir()
|
||||
}
|
||||
dir("${env.WORKSPACE}@2@tmp") {
|
||||
deleteDir()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Jenkinsfile
vendored
Normal file
74
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label 'python311 && amd64'
|
||||
}
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
|
||||
tools {nodejs "Node 20"}
|
||||
|
||||
stages {
|
||||
stage('Install Python Virtual Enviroment') {
|
||||
steps {
|
||||
sh 'python3.11 -m venv env'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Install Application Dependencies') {
|
||||
steps {
|
||||
sh '''
|
||||
. env/bin/activate
|
||||
pip3.11 install --upgrade pip
|
||||
pip3.11 install -r requirements.txt
|
||||
corepack enable
|
||||
npm install
|
||||
deactivate
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh 'npm run build'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy if tagged') {
|
||||
when {
|
||||
tag pattern: '\\d+\\.\\d+\\.\\d+', comparator: "REGEXP"
|
||||
}
|
||||
steps {
|
||||
s3Upload profileName: 'Irrsinn.net Buckets',
|
||||
userMetadata: [],
|
||||
dontWaitForConcurrentBuildCompletion: false,
|
||||
dontSetBuildResultOnFailure: false,
|
||||
pluginFailureResultConstraint: 'FAILURE',
|
||||
consoleLogLevel: 'INFO',
|
||||
entries: [[
|
||||
bucket: 'plex-watchlist',
|
||||
sourceFile:'dist/**',
|
||||
managedArtifacts: false,
|
||||
keepForever: true,
|
||||
noUploadOnFailure: true,
|
||||
selectedRegion: 'us-east-1'
|
||||
]]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
post {
|
||||
cleanup {
|
||||
cleanWs()
|
||||
dir("${env.WORKSPACE}@tmp") {
|
||||
deleteDir()
|
||||
}
|
||||
dir("${env.WORKSPACE}@2") {
|
||||
deleteDir()
|
||||
}
|
||||
dir("${env.WORKSPACE}@2@tmp") {
|
||||
deleteDir()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
# public-plex-watchlist
|
||||
Turn your Plex Watchlist RSS feed into an interface non-Plex folks can search and filter
|
||||
# Public Plex Watchlist
|
||||
|
||||
Turn your Plex Watchlist RSS feed into an interface non-Plex folks can search and filter.
|
||||
|
||||
[Changelog](/CHANGELOG.md)
|
||||
|
||||
45
eslint.config.mjs
Normal file
45
eslint.config.mjs
Normal file
@@ -0,0 +1,45 @@
|
||||
import { fixupConfigRules } from '@eslint/compat';
|
||||
import globals from 'globals';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import js from '@eslint/js';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import sonarjs from 'eslint-plugin-sonarjs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/libs/*.js', 'node_modules/**/*', '.env/**/*', 'dist/**/*'],
|
||||
},
|
||||
sonarjs.configs.recommended,
|
||||
...fixupConfigRules(
|
||||
compat.extends(
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:@tanstack/eslint-plugin-query/recommended'
|
||||
)
|
||||
),
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
modules: true,
|
||||
},
|
||||
},
|
||||
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
6998
package-lock.json
generated
Normal file
6998
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
62
package.json
Normal file
62
package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "public-plex-watchlist",
|
||||
"version": "1.0.0",
|
||||
"description": "Turn your Plex Watchlist RSS feed into an interface non-Plex folks can search and filter.",
|
||||
"source": "src/index.html",
|
||||
"scripts": {
|
||||
"start": "parcel",
|
||||
"build": "parcel build",
|
||||
"lint": "npx eslint . -c eslint.config.mjs",
|
||||
"lint-watch": "nodemon --exec npm run lint",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/averymd/public-plex-watchlist.git"
|
||||
},
|
||||
"keywords": [
|
||||
"plex",
|
||||
"reactjs"
|
||||
],
|
||||
"author": "Melissa Avery-Weir",
|
||||
"license": "Unlicense",
|
||||
"bugs": {
|
||||
"url": "https://github.com/averymd/public-plex-watchlist/issues"
|
||||
},
|
||||
"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",
|
||||
"axios": "^1.7.2",
|
||||
"globals": "^15.6.0",
|
||||
"material-react-table": "^2.13.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"rss-parser": "^3.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.1.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.43.1",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint": "^9.5.0",
|
||||
"eslint-formatter-checkstyle": "^8.40.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc-1434af3d22-20240618",
|
||||
"eslint-plugin-sonarjs": "^1.0.3",
|
||||
"events": "^3.3.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"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",
|
||||
"string_decoder": "^1.3.0",
|
||||
"timers-browserify": "^2.0.12",
|
||||
"url": "^0.11.3"
|
||||
}
|
||||
}
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
detect-secrets==1.5.0
|
||||
pre-commit==3.7.1
|
||||
3
sonar-project.properties
Normal file
3
sonar-project.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
sonar.projectKey=averymd_public-plex-watchlist_3d43ef27-fec8-4c37-9758-6f95ec5635cc
|
||||
sonar.exclusions=env/**,node_modules/**
|
||||
sonar.python.version=3.11
|
||||
12
src/App.js
Normal file
12
src/App.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import Watchlist from './components/Watchlist';
|
||||
|
||||
export default function App() {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Watchlist />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
15
src/api/plexApi.js
Normal file
15
src/api/plexApi.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import Parser from 'rss-parser';
|
||||
|
||||
export async function fetchPlexWatchlistFeed({ pageParam }) {
|
||||
let rssParser = new Parser({
|
||||
customFields: {
|
||||
item: [
|
||||
'media:credit',
|
||||
'media:thumbnail',
|
||||
'media:keywords',
|
||||
'media:rating',
|
||||
],
|
||||
},
|
||||
});
|
||||
return rssParser.parseURL(pageParam);
|
||||
}
|
||||
59
src/components/Watchlist/index.js
Normal file
59
src/components/Watchlist/index.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { fetchPlexWatchlistFeed } from '../../api/plexApi';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import WatchlistTable from '../WatchlistTable';
|
||||
|
||||
export default function Watchlist() {
|
||||
const {
|
||||
data,
|
||||
isPending,
|
||||
isLoading,
|
||||
isLoadingError,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ['watchlistItems'],
|
||||
queryFn: fetchPlexWatchlistFeed,
|
||||
initialPageParam: process.env.BASE_RSS_FEED,
|
||||
getNextPageParam: (lastPage) => lastPage?.paginationLinks?.next,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
let loadMoreItems = useCallback(
|
||||
(containerRefElement) => {
|
||||
if (!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]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isPending && (
|
||||
<WatchlistTable
|
||||
items={flatItems}
|
||||
isLoadingItems={isLoading}
|
||||
isErrorLoading={isLoadingError}
|
||||
isPageLoading={isFetchingNextPage}
|
||||
loadMoreItems={loadMoreItems}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
149
src/components/WatchlistTable/index.js
Normal file
149
src/components/WatchlistTable/index.js
Normal file
@@ -0,0 +1,149 @@
|
||||
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,
|
||||
isLoadingItems,
|
||||
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[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(
|
||||
() => [
|
||||
{
|
||||
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: 125,
|
||||
},
|
||||
{
|
||||
header: 'Summary',
|
||||
accessorKey: 'content',
|
||||
minSize: 200,
|
||||
size: 400,
|
||||
maxSize: 600,
|
||||
grow: false,
|
||||
},
|
||||
{
|
||||
header: 'Keywords',
|
||||
id: 'keywords',
|
||||
accessorFn: (row) => {
|
||||
return row[mediaKeywords].split(',');
|
||||
},
|
||||
Cell: ({ renderedCellValue, row }) => (
|
||||
<Stack direction="row" spacing={0.25} useFlexGap flexWrap="wrap">
|
||||
{getKeywordsForRow(renderedCellValue, row).map((keyword) => (
|
||||
<Chip label={keyword} key={keyword} />
|
||||
))}
|
||||
</Stack>
|
||||
),
|
||||
minSize: 200,
|
||||
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',
|
||||
id: 'mediaRating',
|
||||
accessorFn: (row) => row['media:rating']?.toUpperCase(),
|
||||
filterVariant: 'multi-select',
|
||||
},
|
||||
],
|
||||
[keywordOptions, getKeywordsForRow]
|
||||
);
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
data: items,
|
||||
layoutMode: 'semantic',
|
||||
enableFacetedValues: true,
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
rowVirtualizerInstanceRef,
|
||||
rowVirtualizerOptions: { overscan: 4 },
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
isLoading: isLoadingItems,
|
||||
showProgressBars: isPageLoading,
|
||||
showAlertBanner: isErrorLoading,
|
||||
columnFilters,
|
||||
globalFilter,
|
||||
sorting,
|
||||
},
|
||||
initialState: {
|
||||
showGlobalFilter: true,
|
||||
},
|
||||
});
|
||||
|
||||
return <MaterialReactTable table={table} />;
|
||||
}
|
||||
|
||||
WatchlistTable.propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
isLoadingItems: PropTypes.bool.isRequired,
|
||||
isErrorLoading: PropTypes.bool.isRequired,
|
||||
isPageLoading: PropTypes.bool.isRequired,
|
||||
loadMoreItems: PropTypes.func.isRequired,
|
||||
};
|
||||
15
src/index.html
Normal file
15
src/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Plex Watchlist</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="index.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
12
src/index.js
Normal file
12
src/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import App from './App';
|
||||
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
Reference in New Issue
Block a user