import Badge from '@app/components/Common/Badge'; import Button from '@app/components/Common/Button'; import CachedImage from '@app/components/Common/CachedImage'; import ConfirmButton from '@app/components/Common/ConfirmButton'; import Header from '@app/components/Common/Header'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; import useDebouncedState from '@app/hooks/useDebouncedState'; import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import Error from '@app/pages/_error'; import defineMessages from '@app/utils/defineMessages'; import { ChevronLeftIcon, ChevronRightIcon, MagnifyingGlassIcon, TrashIcon, } from '@heroicons/react/24/solid'; import type { BlacklistItem, BlacklistResultsResponse, } from '@server/interfaces/api/blacklistInterfaces'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import Link from 'next/link'; import { useRouter } from 'next/router'; import type { ChangeEvent } from 'react'; import { useState } from 'react'; import { useInView } from 'react-intersection-observer'; import { FormattedRelativeTime, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; const messages = defineMessages('components.Blacklist', { blacklistsettings: 'Blacklist Settings', blacklistSettingsDescription: 'Manage blacklisted media.', mediaName: 'Name', mediaType: 'Type', mediaTmdbId: 'tmdb Id', blacklistdate: 'date', blacklistedby: '{date} by {user}', blacklistNotFoundError: '{title} is not blacklisted.', }); const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { return (movie as MovieDetails).title !== undefined; }; const Blacklist = () => { const [currentPageSize, setCurrentPageSize] = useState(10); const [searchFilter, debouncedSearchFilter, setSearchFilter] = useDebouncedState(''); const router = useRouter(); const intl = useIntl(); const page = router.query.page ? Number(router.query.page) : 1; const pageIndex = page - 1; const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); const { data, error, mutate: revalidate, } = useSWR( `/api/v1/blacklist/?take=${currentPageSize} &skip=${pageIndex * currentPageSize} ${debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''}`, { refreshInterval: 0, revalidateOnFocus: false, } ); // check if there's no data and no errors in the table // so as to show a spinner inside the table and not refresh the whole component if (!data && error) { return ; } const searchItem = (e: ChangeEvent) => { // Remove the "page" query param from the URL // so that the "skip" query param on line 62 is empty // and the search returns results without skipping items if (router.query.page) router.replace(router.basePath); setSearchFilter(e.target.value as string); }; const hasNextPage = data && data.pageInfo.pages > pageIndex + 1; const hasPrevPage = pageIndex > 0; return ( <> {intl.formatMessage(globalMessages.blacklist)} searchItem(e)} /> {!data ? ( ) : data.results.length === 0 ? ( {intl.formatMessage(globalMessages.noresults)} ) : ( data.results.map((item: BlacklistItem) => { return ( ); }) )} {data && (data?.results.length ?? 0) > 0 && intl.formatMessage(globalMessages.showingresults, { from: pageIndex * currentPageSize + 1, to: data.results.length < currentPageSize ? pageIndex * currentPageSize + data.results.length : (pageIndex + 1) * currentPageSize, total: data.pageInfo.results, strong: (msg: React.ReactNode) => ( {msg} ), })} {intl.formatMessage(globalMessages.resultsperpage, { pageSize: ( { setCurrentPageSize(Number(e.target.value)); router .push({ pathname: router.pathname, query: router.query.userId ? { userId: router.query.userId } : {}, }) .then(() => window.scrollTo(0, 0)); }} value={currentPageSize} className="short inline" > 5 10 25 50 100 ), })} updateQueryParams('page', (page - 1).toString())} > {intl.formatMessage(globalMessages.previous)} updateQueryParams('page', (page + 1).toString())} > {intl.formatMessage(globalMessages.next)} > ); }; export default Blacklist; interface BlacklistedItemProps { item: BlacklistItem; revalidateList: () => void; } const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => { const [isUpdating, setIsUpdating] = useState(false); const { addToast } = useToasts(); const { ref, inView } = useInView({ triggerOnce: true, }); const intl = useIntl(); const { hasPermission } = useUser(); const url = item.mediaType === 'movie' ? `/api/v1/movie/${item.tmdbId}` : `/api/v1/tv/${item.tmdbId}`; const { data: title, error } = useSWR( inView ? url : null ); if (!title && !error) { return ( ); } const removeFromBlacklist = async (tmdbId: number, title?: string) => { setIsUpdating(true); const res = await fetch('/api/v1/blacklist/' + tmdbId, { method: 'DELETE', }); if (res.status === 204) { addToast( {intl.formatMessage(globalMessages.removeFromBlacklistSuccess, { title, strong: (msg: React.ReactNode) => {msg}, })} , { appearance: 'success', autoDismiss: true } ); } else { addToast(intl.formatMessage(globalMessages.blacklistError), { appearance: 'error', autoDismiss: true, }); } revalidateList(); setIsUpdating(false); }; return ( {title && title.backdropPath && ( )} {title && (isMovie(title) ? title.releaseDate : title.firstAirDate )?.slice(0, 4)} {title && (isMovie(title) ? title.title : title.name)} Status {intl.formatMessage(globalMessages.blacklisted)} {item.createdAt && ( {intl.formatMessage(globalMessages.blacklisted)} {intl.formatMessage(messages.blacklistedby, { date: ( ), user: ( {item.user.displayName} ), })} )} {item.mediaType === 'movie' ? ( {intl.formatMessage(globalMessages.movie)} ) : ( {intl.formatMessage(globalMessages.tvshow)} )} {hasPermission(Permission.MANAGE_BLACKLIST) && ( removeFromBlacklist( item.tmdbId, title && (isMovie(title) ? title.title : title.name) ) } confirmText={intl.formatMessage( isUpdating ? globalMessages.deleting : globalMessages.areyousure )} className={`w-full ${ isUpdating ? 'pointer-events-none opacity-50' : '' }`} > {intl.formatMessage(globalMessages.removefromBlacklist)} )} ); };
{data && (data?.results.length ?? 0) > 0 && intl.formatMessage(globalMessages.showingresults, { from: pageIndex * currentPageSize + 1, to: data.results.length < currentPageSize ? pageIndex * currentPageSize + data.results.length : (pageIndex + 1) * currentPageSize, total: data.pageInfo.results, strong: (msg: React.ReactNode) => ( {msg} ), })}