feat: blacklist items from Discover page (#632)

* feat: blacklist media items

re #490

* feat: blacklist media items

* feat: blacklist media items

* style: formatting

* refactor: close the manage slide-over when the media item is removed from the blacklist

* fix: fix media data in the db when blacklisting an item

* refactor: refactor component to accept show boolean

* refactor: hide watchlist button in the media page when it's blacklisted. Also add a blacklist button

* style: formatting

---------

Co-authored-by: JoaquinOlivero <joaquin.olivero@hotmail.com>
This commit is contained in:
Joaquin Olivero
2024-09-16 17:08:12 -03:00
committed by GitHub
parent ee7e91c7c9
commit 818aa60aac
30 changed files with 1941 additions and 352 deletions

View File

@@ -1,7 +1,9 @@
import Spinner from '@app/assets/spinner.svg';
import BlacklistModal from '@app/components/BlacklistModal';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import StatusBadgeMini from '@app/components/Common/StatusBadgeMini';
import Tooltip from '@app/components/Common/Tooltip';
import RequestModal from '@app/components/RequestModal';
import ErrorCard from '@app/components/TitleCard/ErrorCard';
import Placeholder from '@app/components/TitleCard/Placeholder';
@@ -13,6 +15,8 @@ import { withProperties } from '@app/utils/typeHelpers';
import { Transition } from '@headlessui/react';
import {
ArrowDownTrayIcon,
EyeIcon,
EyeSlashIcon,
MinusCircleIcon,
StarIcon,
} from '@heroicons/react/24/outline';
@@ -20,7 +24,7 @@ import { MediaStatus } from '@server/constants/media';
import type { Watchlist } from '@server/entity/Watchlist';
import type { MediaType } from '@server/models/Search';
import Link from 'next/link';
import { Fragment, useCallback, useEffect, useState } from 'react';
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import { mutate } from 'swr';
@@ -65,7 +69,7 @@ const TitleCard = ({
}: TitleCardProps) => {
const isTouch = useIsTouch();
const intl = useIntl();
const { hasPermission } = useUser();
const { user, hasPermission } = useUser();
const [isUpdating, setIsUpdating] = useState(false);
const [currentStatus, setCurrentStatus] = useState(status);
const [showDetail, setShowDetail] = useState(false);
@@ -74,6 +78,8 @@ const TitleCard = ({
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
!isAddedToWatchlist
);
const [showBlacklistModal, setShowBlacklistModal] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
// Just to get the year from the date
if (year) {
@@ -94,6 +100,11 @@ const TitleCard = ({
[]
);
const closeBlacklistModal = useCallback(
() => setShowBlacklistModal(false),
[]
);
const onClickWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true);
try {
@@ -166,6 +177,99 @@ const TitleCard = ({
}
};
const onClickHideItemBtn = async (): Promise<void> => {
setIsUpdating(true);
const topNode = cardRef.current;
if (topNode) {
const res = await fetch('/api/v1/blacklist', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: id,
mediaType,
title,
user: user?.id,
}),
});
if (res.status === 201) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
setCurrentStatus(MediaStatus.BLACKLISTED);
} else if (res.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
setIsUpdating(false);
closeBlacklistModal();
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
};
const onClickShowBlacklistBtn = async (): Promise<void> => {
setIsUpdating(true);
const topNode = cardRef.current;
if (topNode) {
const res = await fetch('/api/v1/blacklist/' + id, {
method: 'DELETE',
});
if (res.status === 204) {
addToast(
<span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
setCurrentStatus(MediaStatus.UNKNOWN);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
setIsUpdating(false);
};
const closeModal = useCallback(() => setShowRequestModal(false), []);
const showRequestButton = hasPermission(
@@ -178,10 +282,15 @@ const TitleCard = ({
{ type: 'or' }
);
const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], {
type: 'or',
});
return (
<div
className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}
data-testid="title-card"
ref={cardRef}
>
<RequestModal
tmdbId={id}
@@ -197,6 +306,20 @@ const TitleCard = ({
onUpdating={requestUpdating}
onCancel={closeModal}
/>
<BlacklistModal
tmdbId={id}
type={
mediaType === 'movie'
? 'movie'
: mediaType === 'collection'
? 'collection'
: 'tv'
}
show={showBlacklistModal}
onCancel={closeBlacklistModal}
onComplete={onClickHideItemBtn}
isUpdating={isUpdating}
/>
<div
className={`relative transform-gpu cursor-default overflow-hidden rounded-xl bg-gray-800 bg-cover outline-none ring-1 transition duration-300 ${
showDetail
@@ -235,7 +358,7 @@ const TitleCard = ({
/>
<div className="absolute left-0 right-0 flex items-center justify-between p-2">
<div
className={`pointer-events-none z-40 rounded-full border bg-opacity-80 shadow-md ${
className={`pointer-events-none z-40 self-start rounded-full border bg-opacity-80 shadow-md ${
mediaType === 'movie' || mediaType === 'collection'
? 'border-blue-500 bg-blue-600'
: 'border-purple-600 bg-purple-600'
@@ -249,8 +372,8 @@ const TitleCard = ({
: intl.formatMessage(globalMessages.tvshow)}
</div>
</div>
{showDetail && (
<>
{showDetail && currentStatus !== MediaStatus.BLACKLISTED && (
<div className="flex flex-col gap-1">
{toggleWatchlist ? (
<Button
buttonType={'ghost'}
@@ -269,15 +392,49 @@ const TitleCard = ({
<MinusCircleIcon className={'h-3'} />
</Button>
)}
</>
{showHideButton &&
currentStatus !== MediaStatus.PROCESSING &&
currentStatus !== MediaStatus.AVAILABLE &&
currentStatus !== MediaStatus.PARTIALLY_AVAILABLE &&
currentStatus !== MediaStatus.PENDING && (
<Button
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={() => setShowBlacklistModal(true)}
>
<EyeSlashIcon className={'h-3'} />
</Button>
)}
</div>
)}
{showDetail &&
showHideButton &&
currentStatus == MediaStatus.BLACKLISTED && (
<Tooltip
content={intl.formatMessage(
globalMessages.removefromBlacklist
)}
>
<Button
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={() => onClickShowBlacklistBtn()}
>
<EyeIcon className={'h-3'} />
</Button>
</Tooltip>
)}
{currentStatus && currentStatus !== MediaStatus.UNKNOWN && (
<div className="pointer-events-none z-40 flex items-center">
<StatusBadgeMini
status={currentStatus}
inProgress={inProgress}
shrink
/>
<div className="flex flex-col items-center gap-1">
<div className="pointer-events-none z-40 flex">
<StatusBadgeMini
status={currentStatus}
inProgress={inProgress}
shrink
/>
</div>
</div>
)}
</div>