Skip to content

Commit

Permalink
feat: adds button on the page of a media item to add or remove it fro…
Browse files Browse the repository at this point in the history
…m the user's watchlist

re #730
  • Loading branch information
JoaquinOlivero committed May 30, 2024
1 parent c3ddc86 commit 054da1d
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 9 deletions.
5 changes: 4 additions & 1 deletion server/models/Movie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export interface MovieDetails {
mediaUrl?: string;
watchProviders?: WatchProviders[];
keywords: Keyword[];
onUserWatchlist?: boolean;
}

export const mapProductionCompany = (
Expand All @@ -101,7 +102,8 @@ export const mapProductionCompany = (

export const mapMovieDetails = (
movie: TmdbMovieDetails,
media?: Media
media?: Media,
userWatchlist?: boolean
): MovieDetails => ({
id: movie.id,
adult: movie.adult,
Expand Down Expand Up @@ -148,4 +150,5 @@ export const mapMovieDetails = (
id: keyword.id,
name: keyword.name,
})),
onUserWatchlist: userWatchlist,
});
5 changes: 4 additions & 1 deletion server/models/Tv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export interface TvDetails {
keywords: Keyword[];
mediaInfo?: Media;
watchProviders?: WatchProviders[];
onUserWatchlist?: boolean;
}

const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
Expand Down Expand Up @@ -161,7 +162,8 @@ export const mapNetwork = (network: TmdbNetwork): TvNetwork => ({

export const mapTvDetails = (
show: TmdbTvDetails,
media?: Media
media?: Media,
userWatchlist?: boolean
): TvDetails => ({
createdBy: show.created_by,
episodeRunTime: show.episode_run_time,
Expand Down Expand Up @@ -223,4 +225,5 @@ export const mapTvDetails = (
})),
mediaInfo: media,
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
onUserWatchlist: userWatchlist,
});
15 changes: 14 additions & 1 deletion server/routes/movie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import RottenTomatoes from '@server/api/rating/rottentomatoes';
import { type RatingResponse } from '@server/api/ratings';
import TheMovieDb from '@server/api/themoviedb';
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import { Watchlist } from '@server/entity/Watchlist';
import logger from '@server/logger';
import { mapMovieDetails } from '@server/models/Movie';
import { mapMovieResult } from '@server/models/Search';
Expand All @@ -22,7 +24,18 @@ movieRoutes.get('/:id', async (req, res, next) => {

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) {
logger.debug('Something went wrong retrieving movie', {
label: 'API',
Expand Down
13 changes: 12 additions & 1 deletion server/routes/tv.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import RottenTomatoes from '@server/api/rating/rottentomatoes';
import TheMovieDb from '@server/api/themoviedb';
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import { Watchlist } from '@server/entity/Watchlist';
import logger from '@server/logger';
import { mapTvResult } from '@server/models/Search';
import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv';
Expand All @@ -19,7 +21,16 @@ tvRoutes.get('/:id', async (req, res, next) => {

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) {
logger.debug('Something went wrong retrieving series', {
label: 'API',
Expand Down
113 changes: 112 additions & 1 deletion src/components/MovieDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import RTAudRotten from '@app/assets/rt_aud_rotten.svg';
import RTFresh from '@app/assets/rt_fresh.svg';
import RTRotten from '@app/assets/rt_rotten.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 Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
Expand Down Expand Up @@ -40,12 +41,16 @@ import {
import {
ChevronDoubleDownIcon,
ChevronDoubleUpIcon,
MinusCircleIcon,
StarIcon,
} from '@heroicons/react/24/solid';
import { type RatingResponse } from '@server/api/ratings';
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 type { Watchlist } from '@server/entity/Watchlist';
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
import axios from 'axios';
import { hasFlag } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css';
import { uniqBy } from 'lodash';
Expand All @@ -54,6 +59,7 @@ import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';

const messages = defineMessages({
Expand Down Expand Up @@ -93,6 +99,13 @@ const messages = defineMessages({
rtaudiencescore: 'Rotten Tomatoes Audience Score',
tmdbuserscore: 'TMDB 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 {
Expand All @@ -111,7 +124,12 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
const minStudios = 3;
const [showMoreStudios, setShowMoreStudios] = useState(false);
const [showIssueModal, setShowIssueModal] = useState(false);
const [isUpdating, setIsUpdating] = useState<boolean>(false);
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
!movie?.onUserWatchlist
);
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();

const {
data,
Expand Down Expand Up @@ -282,6 +300,65 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
}

const onClickWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true);
try {
const response = await axios.post<Watchlist>('/api/v1/watchlist', {
tmdbId: movie?.id,
mediaType: MediaType.MOVIE,
title: movie?.title,
});
if (response.data) {
addToast(
<span>
{intl.formatMessage(messages.watchlistSuccess, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
}
} catch (e) {
addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error',
autoDismiss: true,
});
} finally {
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 (
<div
className="media-page"
Expand Down Expand Up @@ -402,6 +479,40 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
</span>
</div>
<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} />
<RequestButton
mediaType="movie"
Expand Down
Loading

0 comments on commit 054da1d

Please sign in to comment.