mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
feat: Option on item's page to add/remove from watchlist (#781)
* feat: adds button on the page of a media item to add or remove it from a user's watchlist re #730 * fix: whitespace and i18n key * style: fix code format to the required standards * refactor: change axios for the fetch api --------- Co-authored-by: JoaquinOlivero <joaquin.olivero@hotmail.com>
This commit is contained in:
@@ -85,6 +85,7 @@ export interface MovieDetails {
|
|||||||
mediaUrl?: string;
|
mediaUrl?: string;
|
||||||
watchProviders?: WatchProviders[];
|
watchProviders?: WatchProviders[];
|
||||||
keywords: Keyword[];
|
keywords: Keyword[];
|
||||||
|
onUserWatchlist?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapProductionCompany = (
|
export const mapProductionCompany = (
|
||||||
@@ -101,7 +102,8 @@ export const mapProductionCompany = (
|
|||||||
|
|
||||||
export const mapMovieDetails = (
|
export const mapMovieDetails = (
|
||||||
movie: TmdbMovieDetails,
|
movie: TmdbMovieDetails,
|
||||||
media?: Media
|
media?: Media,
|
||||||
|
userWatchlist?: boolean
|
||||||
): MovieDetails => ({
|
): MovieDetails => ({
|
||||||
id: movie.id,
|
id: movie.id,
|
||||||
adult: movie.adult,
|
adult: movie.adult,
|
||||||
@@ -148,4 +150,5 @@ export const mapMovieDetails = (
|
|||||||
id: keyword.id,
|
id: keyword.id,
|
||||||
name: keyword.name,
|
name: keyword.name,
|
||||||
})),
|
})),
|
||||||
|
onUserWatchlist: userWatchlist,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export interface TvDetails {
|
|||||||
keywords: Keyword[];
|
keywords: Keyword[];
|
||||||
mediaInfo?: Media;
|
mediaInfo?: Media;
|
||||||
watchProviders?: WatchProviders[];
|
watchProviders?: WatchProviders[];
|
||||||
|
onUserWatchlist?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
|
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
|
||||||
@@ -161,7 +162,8 @@ export const mapNetwork = (network: TmdbNetwork): TvNetwork => ({
|
|||||||
|
|
||||||
export const mapTvDetails = (
|
export const mapTvDetails = (
|
||||||
show: TmdbTvDetails,
|
show: TmdbTvDetails,
|
||||||
media?: Media
|
media?: Media,
|
||||||
|
userWatchlist?: boolean
|
||||||
): TvDetails => ({
|
): TvDetails => ({
|
||||||
createdBy: show.created_by,
|
createdBy: show.created_by,
|
||||||
episodeRunTime: show.episode_run_time,
|
episodeRunTime: show.episode_run_time,
|
||||||
@@ -223,4 +225,5 @@ export const mapTvDetails = (
|
|||||||
})),
|
})),
|
||||||
mediaInfo: media,
|
mediaInfo: media,
|
||||||
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
|
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
|
||||||
|
onUserWatchlist: userWatchlist,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
|||||||
import { type RatingResponse } from '@server/api/ratings';
|
import { type RatingResponse } from '@server/api/ratings';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
|
import { Watchlist } from '@server/entity/Watchlist';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { mapMovieDetails } from '@server/models/Movie';
|
import { mapMovieDetails } from '@server/models/Movie';
|
||||||
import { mapMovieResult } from '@server/models/Search';
|
import { mapMovieResult } from '@server/models/Search';
|
||||||
@@ -22,7 +24,18 @@ movieRoutes.get('/:id', async (req, res, next) => {
|
|||||||
|
|
||||||
const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE);
|
const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE);
|
||||||
|
|
||||||
return res.status(200).json(mapMovieDetails(tmdbMovie, media));
|
const onUserWatchlist = await getRepository(Watchlist).exist({
|
||||||
|
where: {
|
||||||
|
tmdbId: Number(req.params.id),
|
||||||
|
requestedBy: {
|
||||||
|
id: req.user?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res
|
||||||
|
.status(200)
|
||||||
|
.json(mapMovieDetails(tmdbMovie, media, onUserWatchlist));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Something went wrong retrieving movie', {
|
logger.debug('Something went wrong retrieving movie', {
|
||||||
label: 'API',
|
label: 'API',
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
|
import { Watchlist } from '@server/entity/Watchlist';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { mapTvResult } from '@server/models/Search';
|
import { mapTvResult } from '@server/models/Search';
|
||||||
import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv';
|
import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv';
|
||||||
@@ -19,7 +21,16 @@ tvRoutes.get('/:id', async (req, res, next) => {
|
|||||||
|
|
||||||
const media = await Media.getMedia(tv.id, MediaType.TV);
|
const media = await Media.getMedia(tv.id, MediaType.TV);
|
||||||
|
|
||||||
return res.status(200).json(mapTvDetails(tv, media));
|
const onUserWatchlist = await getRepository(Watchlist).exist({
|
||||||
|
where: {
|
||||||
|
tmdbId: Number(req.params.id),
|
||||||
|
requestedBy: {
|
||||||
|
id: req.user?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(mapTvDetails(tv, media, onUserWatchlist));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Something went wrong retrieving series', {
|
logger.debug('Something went wrong retrieving series', {
|
||||||
label: 'API',
|
label: 'API',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import RTAudRotten from '@app/assets/rt_aud_rotten.svg';
|
|||||||
import RTFresh from '@app/assets/rt_fresh.svg';
|
import RTFresh from '@app/assets/rt_fresh.svg';
|
||||||
import RTRotten from '@app/assets/rt_rotten.svg';
|
import RTRotten from '@app/assets/rt_rotten.svg';
|
||||||
import ImdbLogo from '@app/assets/services/imdb.svg';
|
import ImdbLogo from '@app/assets/services/imdb.svg';
|
||||||
|
import Spinner from '@app/assets/spinner.svg';
|
||||||
import TmdbLogo from '@app/assets/tmdb_logo.svg';
|
import TmdbLogo from '@app/assets/tmdb_logo.svg';
|
||||||
import Button from '@app/components/Common/Button';
|
import Button from '@app/components/Common/Button';
|
||||||
import CachedImage from '@app/components/Common/CachedImage';
|
import CachedImage from '@app/components/Common/CachedImage';
|
||||||
@@ -41,12 +42,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
ChevronDoubleDownIcon,
|
ChevronDoubleDownIcon,
|
||||||
ChevronDoubleUpIcon,
|
ChevronDoubleUpIcon,
|
||||||
|
MinusCircleIcon,
|
||||||
|
StarIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { type RatingResponse } from '@server/api/ratings';
|
import { type RatingResponse } from '@server/api/ratings';
|
||||||
import { IssueStatus } from '@server/constants/issue';
|
import { IssueStatus } from '@server/constants/issue';
|
||||||
import { MediaStatus } from '@server/constants/media';
|
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import type { Watchlist } from '@server/entity/Watchlist';
|
||||||
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
|
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
|
||||||
|
import axios from 'axios';
|
||||||
import { countries } from 'country-flag-icons';
|
import { countries } from 'country-flag-icons';
|
||||||
import 'country-flag-icons/3x2/flags.css';
|
import 'country-flag-icons/3x2/flags.css';
|
||||||
import { uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
@@ -55,6 +60,7 @@ import Link from 'next/link';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
const messages = defineMessages('components.MovieDetails', {
|
const messages = defineMessages('components.MovieDetails', {
|
||||||
@@ -94,6 +100,12 @@ const messages = defineMessages('components.MovieDetails', {
|
|||||||
rtaudiencescore: 'Rotten Tomatoes Audience Score',
|
rtaudiencescore: 'Rotten Tomatoes Audience Score',
|
||||||
tmdbuserscore: 'TMDB User Score',
|
tmdbuserscore: 'TMDB User Score',
|
||||||
imdbuserscore: 'IMDB User Score',
|
imdbuserscore: 'IMDB User Score',
|
||||||
|
watchlistSuccess: '<strong>{title}</strong> added to watchlist successfully!',
|
||||||
|
watchlistDeleted:
|
||||||
|
'<strong>{title}</strong> Removed from watchlist successfully!',
|
||||||
|
watchlistError: 'Something went wrong try again.',
|
||||||
|
removefromwatchlist: 'Remove From Watchlist',
|
||||||
|
addtowatchlist: 'Add To Watchlist',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface MovieDetailsProps {
|
interface MovieDetailsProps {
|
||||||
@@ -112,7 +124,12 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
const minStudios = 3;
|
const minStudios = 3;
|
||||||
const [showMoreStudios, setShowMoreStudios] = useState(false);
|
const [showMoreStudios, setShowMoreStudios] = useState(false);
|
||||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||||
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
|
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
|
||||||
|
!movie?.onUserWatchlist
|
||||||
|
);
|
||||||
const { publicRuntimeConfig } = getConfig();
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -287,6 +304,79 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
|
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onClickWatchlistBtn = async (): Promise<void> => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
|
||||||
|
const res = await fetch('/api/v1/watchlist', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
tmdbId: movie?.id,
|
||||||
|
mediaType: MediaType.MOVIE,
|
||||||
|
title: movie?.title,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
addToast(intl.formatMessage(messages.watchlistError), {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsUpdating(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
addToast(
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage(messages.watchlistSuccess, {
|
||||||
|
title: movie?.title,
|
||||||
|
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
||||||
|
})}
|
||||||
|
</span>,
|
||||||
|
{ appearance: 'success', autoDismiss: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUpdating(false);
|
||||||
|
setToggleWatchlist((prevState) => !prevState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.delete<Watchlist>(
|
||||||
|
'/api/v1/watchlist/' + movie?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
addToast(
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage(messages.watchlistDeleted, {
|
||||||
|
title: movie?.title,
|
||||||
|
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
||||||
|
})}
|
||||||
|
</span>,
|
||||||
|
{ appearance: 'info', autoDismiss: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
addToast(intl.formatMessage(messages.watchlistError), {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
setToggleWatchlist((prevState) => !prevState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="media-page"
|
className="media-page"
|
||||||
@@ -408,6 +498,40 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-actions">
|
<div className="media-actions">
|
||||||
|
<>
|
||||||
|
{toggleWatchlist ? (
|
||||||
|
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
|
||||||
|
<Button
|
||||||
|
buttonType={'ghost'}
|
||||||
|
className="z-40 mr-2"
|
||||||
|
buttonSize={'md'}
|
||||||
|
onClick={onClickWatchlistBtn}
|
||||||
|
>
|
||||||
|
{isUpdating ? (
|
||||||
|
<Spinner className="h-3" />
|
||||||
|
) : (
|
||||||
|
<StarIcon className={'h-3 text-amber-300'} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip
|
||||||
|
content={intl.formatMessage(messages.removefromwatchlist)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="z-40 mr-2"
|
||||||
|
buttonSize={'md'}
|
||||||
|
onClick={onClickDeleteWatchlistBtn}
|
||||||
|
>
|
||||||
|
{isUpdating ? (
|
||||||
|
<Spinner className="h-3" />
|
||||||
|
) : (
|
||||||
|
<MinusCircleIcon className={'h-3'} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
<PlayButton links={mediaLinks} />
|
<PlayButton links={mediaLinks} />
|
||||||
<RequestButton
|
<RequestButton
|
||||||
mediaType="movie"
|
mediaType="movie"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import RTAudFresh from '@app/assets/rt_aud_fresh.svg';
|
|||||||
import RTAudRotten from '@app/assets/rt_aud_rotten.svg';
|
import RTAudRotten from '@app/assets/rt_aud_rotten.svg';
|
||||||
import RTFresh from '@app/assets/rt_fresh.svg';
|
import RTFresh from '@app/assets/rt_fresh.svg';
|
||||||
import RTRotten from '@app/assets/rt_rotten.svg';
|
import RTRotten from '@app/assets/rt_rotten.svg';
|
||||||
|
import Spinner from '@app/assets/spinner.svg';
|
||||||
import TmdbLogo from '@app/assets/tmdb_logo.svg';
|
import TmdbLogo from '@app/assets/tmdb_logo.svg';
|
||||||
import Badge from '@app/components/Common/Badge';
|
import Badge from '@app/components/Common/Badge';
|
||||||
import Button from '@app/components/Common/Button';
|
import Button from '@app/components/Common/Button';
|
||||||
@@ -40,11 +41,19 @@ import {
|
|||||||
FilmIcon,
|
FilmIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { ChevronDownIcon } from '@heroicons/react/24/solid';
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
MinusCircleIcon,
|
||||||
|
StarIcon,
|
||||||
|
} from '@heroicons/react/24/solid';
|
||||||
import type { RTRating } from '@server/api/rating/rottentomatoes';
|
import type { RTRating } from '@server/api/rating/rottentomatoes';
|
||||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||||
import { IssueStatus } from '@server/constants/issue';
|
import { IssueStatus } from '@server/constants/issue';
|
||||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
import {
|
||||||
|
MediaRequestStatus,
|
||||||
|
MediaStatus,
|
||||||
|
MediaType,
|
||||||
|
} from '@server/constants/media';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import type { Crew } from '@server/models/common';
|
import type { Crew } from '@server/models/common';
|
||||||
import type { TvDetails as TvDetailsType } from '@server/models/Tv';
|
import type { TvDetails as TvDetailsType } from '@server/models/Tv';
|
||||||
@@ -55,6 +64,7 @@ import Link from 'next/link';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
const messages = defineMessages('components.TvDetails', {
|
const messages = defineMessages('components.TvDetails', {
|
||||||
@@ -89,6 +99,12 @@ const messages = defineMessages('components.TvDetails', {
|
|||||||
rtcriticsscore: 'Rotten Tomatoes Tomatometer',
|
rtcriticsscore: 'Rotten Tomatoes Tomatometer',
|
||||||
rtaudiencescore: 'Rotten Tomatoes Audience Score',
|
rtaudiencescore: 'Rotten Tomatoes Audience Score',
|
||||||
tmdbuserscore: 'TMDB User Score',
|
tmdbuserscore: 'TMDB User Score',
|
||||||
|
watchlistSuccess: '<strong>{title}</strong> added to watchlist successfully!',
|
||||||
|
watchlistDeleted:
|
||||||
|
'<strong>{title}</strong> Removed from watchlist successfully!',
|
||||||
|
watchlistError: 'Something went wrong try again.',
|
||||||
|
removefromwatchlist: 'Remove From Watchlist',
|
||||||
|
addtowatchlist: 'Add To Watchlist',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface TvDetailsProps {
|
interface TvDetailsProps {
|
||||||
@@ -106,7 +122,12 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
router.query.manage == '1' ? true : false
|
router.query.manage == '1' ? true : false
|
||||||
);
|
);
|
||||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||||
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
|
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
|
||||||
|
!tv?.onUserWatchlist
|
||||||
|
);
|
||||||
const { publicRuntimeConfig } = getConfig();
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -302,6 +323,82 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
|
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onClickWatchlistBtn = async (): Promise<void> => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
|
||||||
|
const res = await fetch('/api/v1/watchlist', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
tmdbId: tv?.id,
|
||||||
|
mediaType: MediaType.TV,
|
||||||
|
title: tv?.name,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
addToast(intl.formatMessage(messages.watchlistError), {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsUpdating(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
addToast(
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage(messages.watchlistSuccess, {
|
||||||
|
title: tv?.name,
|
||||||
|
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
||||||
|
})}
|
||||||
|
</span>,
|
||||||
|
{ appearance: 'success', autoDismiss: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUpdating(false);
|
||||||
|
setToggleWatchlist((prevState) => !prevState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
|
||||||
|
const res = await fetch('/api/v1/watchlist/' + tv?.id, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
addToast(intl.formatMessage(messages.watchlistError), {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsUpdating(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 204) {
|
||||||
|
addToast(
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage(messages.watchlistDeleted, {
|
||||||
|
title: tv?.name,
|
||||||
|
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
||||||
|
})}
|
||||||
|
</span>,
|
||||||
|
{ appearance: 'info', autoDismiss: true }
|
||||||
|
);
|
||||||
|
setIsUpdating(false);
|
||||||
|
setToggleWatchlist((prevState) => !prevState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="media-page"
|
className="media-page"
|
||||||
@@ -433,6 +530,40 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-actions">
|
<div className="media-actions">
|
||||||
|
<>
|
||||||
|
{toggleWatchlist ? (
|
||||||
|
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
|
||||||
|
<Button
|
||||||
|
buttonType={'ghost'}
|
||||||
|
className="z-40 mr-2"
|
||||||
|
buttonSize={'md'}
|
||||||
|
onClick={onClickWatchlistBtn}
|
||||||
|
>
|
||||||
|
{isUpdating ? (
|
||||||
|
<Spinner className="h-3" />
|
||||||
|
) : (
|
||||||
|
<StarIcon className={'h-3 text-amber-300'} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip
|
||||||
|
content={intl.formatMessage(messages.removefromwatchlist)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="z-40 mr-2"
|
||||||
|
buttonSize={'md'}
|
||||||
|
onClick={onClickDeleteWatchlistBtn}
|
||||||
|
>
|
||||||
|
{isUpdating ? (
|
||||||
|
<Spinner className="h-3" />
|
||||||
|
) : (
|
||||||
|
<MinusCircleIcon className={'h-3'} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
<PlayButton links={mediaLinks} />
|
<PlayButton links={mediaLinks} />
|
||||||
<RequestButton
|
<RequestButton
|
||||||
mediaType="tv"
|
mediaType="tv"
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
"components.MediaSlider.ShowMoreCard.seemore": "See More",
|
"components.MediaSlider.ShowMoreCard.seemore": "See More",
|
||||||
"components.MovieDetails.MovieCast.fullcast": "Full Cast",
|
"components.MovieDetails.MovieCast.fullcast": "Full Cast",
|
||||||
"components.MovieDetails.MovieCrew.fullcrew": "Full Crew",
|
"components.MovieDetails.MovieCrew.fullcrew": "Full Crew",
|
||||||
|
"components.MovieDetails.addtowatchlist": "Add To Watchlist",
|
||||||
"components.MovieDetails.budget": "Budget",
|
"components.MovieDetails.budget": "Budget",
|
||||||
"components.MovieDetails.cast": "Cast",
|
"components.MovieDetails.cast": "Cast",
|
||||||
"components.MovieDetails.digitalrelease": "Digital Release",
|
"components.MovieDetails.digitalrelease": "Digital Release",
|
||||||
@@ -306,6 +307,7 @@
|
|||||||
"components.MovieDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
|
"components.MovieDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
|
||||||
"components.MovieDetails.recommendations": "Recommendations",
|
"components.MovieDetails.recommendations": "Recommendations",
|
||||||
"components.MovieDetails.releasedate": "{releaseCount, plural, one {Release Date} other {Release Dates}}",
|
"components.MovieDetails.releasedate": "{releaseCount, plural, one {Release Date} other {Release Dates}}",
|
||||||
|
"components.MovieDetails.removefromwatchlist": "Remove From Watchlist",
|
||||||
"components.MovieDetails.reportissue": "Report an Issue",
|
"components.MovieDetails.reportissue": "Report an Issue",
|
||||||
"components.MovieDetails.revenue": "Revenue",
|
"components.MovieDetails.revenue": "Revenue",
|
||||||
"components.MovieDetails.rtaudiencescore": "Rotten Tomatoes Audience Score",
|
"components.MovieDetails.rtaudiencescore": "Rotten Tomatoes Audience Score",
|
||||||
@@ -319,6 +321,9 @@
|
|||||||
"components.MovieDetails.theatricalrelease": "Theatrical Release",
|
"components.MovieDetails.theatricalrelease": "Theatrical Release",
|
||||||
"components.MovieDetails.tmdbuserscore": "TMDB User Score",
|
"components.MovieDetails.tmdbuserscore": "TMDB User Score",
|
||||||
"components.MovieDetails.viewfullcrew": "View Full Crew",
|
"components.MovieDetails.viewfullcrew": "View Full Crew",
|
||||||
|
"components.MovieDetails.watchlistDeleted": "<strong>{title}</strong> Removed from watchlist successfully!",
|
||||||
|
"components.MovieDetails.watchlistError": "Something went wrong try again.",
|
||||||
|
"components.MovieDetails.watchlistSuccess": "<strong>{title}</strong> added to watchlist successfully!",
|
||||||
"components.MovieDetails.watchtrailer": "Watch Trailer",
|
"components.MovieDetails.watchtrailer": "Watch Trailer",
|
||||||
"components.NotificationTypeSelector.adminissuecommentDescription": "Get notified when other users comment on issues.",
|
"components.NotificationTypeSelector.adminissuecommentDescription": "Get notified when other users comment on issues.",
|
||||||
"components.NotificationTypeSelector.adminissuereopenedDescription": "Get notified when issues are reopened by other users.",
|
"components.NotificationTypeSelector.adminissuereopenedDescription": "Get notified when issues are reopened by other users.",
|
||||||
@@ -1071,6 +1076,7 @@
|
|||||||
"components.TvDetails.Season.somethingwentwrong": "Something went wrong while retrieving season data.",
|
"components.TvDetails.Season.somethingwentwrong": "Something went wrong while retrieving season data.",
|
||||||
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
|
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
|
||||||
"components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew",
|
"components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew",
|
||||||
|
"components.TvDetails.addtowatchlist": "Add To Watchlist",
|
||||||
"components.TvDetails.anime": "Anime",
|
"components.TvDetails.anime": "Anime",
|
||||||
"components.TvDetails.cast": "Cast",
|
"components.TvDetails.cast": "Cast",
|
||||||
"components.TvDetails.episodeCount": "{episodeCount, plural, one {# Episode} other {# Episodes}}",
|
"components.TvDetails.episodeCount": "{episodeCount, plural, one {# Episode} other {# Episodes}}",
|
||||||
@@ -1088,6 +1094,7 @@
|
|||||||
"components.TvDetails.play4k": "Play 4K on {mediaServerName}",
|
"components.TvDetails.play4k": "Play 4K on {mediaServerName}",
|
||||||
"components.TvDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
|
"components.TvDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
|
||||||
"components.TvDetails.recommendations": "Recommendations",
|
"components.TvDetails.recommendations": "Recommendations",
|
||||||
|
"components.TvDetails.removefromwatchlist": "Remove From Watchlist",
|
||||||
"components.TvDetails.reportissue": "Report an Issue",
|
"components.TvDetails.reportissue": "Report an Issue",
|
||||||
"components.TvDetails.rtaudiencescore": "Rotten Tomatoes Audience Score",
|
"components.TvDetails.rtaudiencescore": "Rotten Tomatoes Audience Score",
|
||||||
"components.TvDetails.rtcriticsscore": "Rotten Tomatoes Tomatometer",
|
"components.TvDetails.rtcriticsscore": "Rotten Tomatoes Tomatometer",
|
||||||
@@ -1100,6 +1107,9 @@
|
|||||||
"components.TvDetails.streamingproviders": "Currently Streaming On",
|
"components.TvDetails.streamingproviders": "Currently Streaming On",
|
||||||
"components.TvDetails.tmdbuserscore": "TMDB User Score",
|
"components.TvDetails.tmdbuserscore": "TMDB User Score",
|
||||||
"components.TvDetails.viewfullcrew": "View Full Crew",
|
"components.TvDetails.viewfullcrew": "View Full Crew",
|
||||||
|
"components.TvDetails.watchlistDeleted": "<strong>{title}</strong> Removed from watchlist successfully!",
|
||||||
|
"components.TvDetails.watchlistError": "Something went wrong try again.",
|
||||||
|
"components.TvDetails.watchlistSuccess": "<strong>{title}</strong> added to watchlist successfully!",
|
||||||
"components.TvDetails.watchtrailer": "Watch Trailer",
|
"components.TvDetails.watchtrailer": "Watch Trailer",
|
||||||
"components.UserList.accounttype": "Type",
|
"components.UserList.accounttype": "Type",
|
||||||
"components.UserList.admin": "Admin",
|
"components.UserList.admin": "Admin",
|
||||||
|
|||||||
Reference in New Issue
Block a user