feat: tv request modal status hookup

pull/132/head
sct 4 years ago
parent 85ae4998f0
commit 5f8114f730

@ -45,6 +45,7 @@ class Media {
try {
const media = await mediaRepository.findOne({
where: { tmdbId: id },
relations: ['requests'],
});
return media;
@ -75,7 +76,7 @@ class Media {
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
public status: MediaStatus;
@OneToMany(() => MediaRequest, (request) => request.media)
@OneToMany(() => MediaRequest, (request) => request.media, { cascade: true })
public requests: MediaRequest[];
@CreateDateColumn()

@ -27,7 +27,9 @@ export class MediaRequest {
@Column({ type: 'integer' })
public status: MediaRequestStatus;
@ManyToOne(() => Media, (media) => media.requests, { eager: true })
@ManyToOne(() => Media, (media) => media.requests, {
eager: true,
})
public media: Media;
@ManyToOne(() => User, (user) => user.requests, { eager: true })

@ -108,7 +108,9 @@ requestRoutes.post(
const request = new MediaRequest({
type: MediaType.TV,
media,
media: {
id: media.id,
} as Media,
requestedBy: req.user,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(Permission.AUTO_APPROVE)

@ -0,0 +1,29 @@
import React from 'react';
interface BadgeProps {
badgeType?: 'default' | 'primary' | 'danger' | 'warning' | 'success';
}
const Badge: React.FC<BadgeProps> = ({ badgeType = 'default', children }) => {
const badgeStyle = [
'px-2 inline-flex text-xs leading-5 font-semibold rounded-full',
];
switch (badgeType) {
case 'danger':
badgeStyle.push('bg-red-600 text-red-100');
break;
case 'warning':
badgeStyle.push('bg-orange-400 text-orange-100');
break;
case 'success':
badgeStyle.push('bg-green-500 text-green-100');
break;
default:
badgeStyle.push('bg-indigo-500 text-indigo-100');
}
return <span className={badgeStyle.join(' ')}>{children}</span>;
};
export default Badge;

@ -20,6 +20,7 @@ import LoadingSpinner from '../Common/LoadingSpinner';
import { useUser, Permission } from '../../hooks/useUser';
import { MediaStatus } from '../../../server/constants/media';
import RequestModal from '../RequestModal';
import Badge from '../Common/Badge';
const messages = defineMessages({
releasedate: 'Release Date',
@ -98,7 +99,6 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
tmdbId={data.id}
show={showRequestModal}
type="movie"
requestId={data.mediaInfo?.requests?.[0]?.id}
onComplete={() => {
revalidate();
setShowRequestModal(false);
@ -114,10 +114,21 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
/>
</div>
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
<span className="md:text-2xl md:leading-none">
{data.releaseDate.slice(0, 4)}
</span>
<h1 className="text-2xl md:text-4xl">{data.title}</h1>
<div className="mb-2 md:mb-0">
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Badge badgeType="danger">Unavailable</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
)}
</div>
<h1 className="text-2xl md:text-4xl">
{data.title}{' '}
<span className="text-2xl">({data.releaseDate.slice(0, 4)})</span>
</h1>
<span className="text-xs md:text-base mt-1 md:mt-0">
{(data.runtime ?? 0) > 0 && (
<>

@ -7,8 +7,13 @@ import { MediaRequest } from '../../../server/entity/MediaRequest';
import useSWR from 'swr';
import { useToasts } from 'react-toast-notifications';
import axios from 'axios';
import type { MediaStatus } from '../../../server/constants/media';
import {
MediaStatus,
MediaRequestStatus,
} from '../../../server/constants/media';
import { TvDetails, SeasonWithEpisodes } from '../../../server/models/Tv';
import type SeasonRequest from '../../../server/entity/SeasonRequest';
import Badge from '../Common/Badge';
const messages = defineMessages({
requestadmin: 'Your request will be immediately approved.',
@ -71,11 +76,24 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
}
};
const getAllRequestedSeasons = (): number[] =>
(data?.mediaInfo?.requests ?? []).reduce((requestedSeasons, request) => {
return [
...requestedSeasons,
...request.seasons.map((sr) => sr.seasonNumber),
];
}, [] as number[]);
const isSelectedSeason = (seasonNumber: number): boolean => {
return selectedSeasons.includes(seasonNumber);
};
const toggleSeason = (seasonNumber: number): void => {
// If this season already has a pending request, don't allow it to be toggled
if (getAllRequestedSeasons().includes(seasonNumber)) {
return;
}
if (selectedSeasons.includes(seasonNumber)) {
setSelectedSeasons((seasons) =>
seasons.filter((sn) => sn !== seasonNumber)
@ -90,11 +108,18 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
data &&
selectedSeasons.length >= 0 &&
selectedSeasons.length <
data?.seasons.filter((season) => season.seasonNumber !== 0).length
data?.seasons
.filter((season) => season.seasonNumber !== 0)
.filter(
(season) => !getAllRequestedSeasons().includes(season.seasonNumber)
).length
) {
setSelectedSeasons(
data.seasons
.filter((season) => season.seasonNumber !== 0)
.filter(
(season) => !getAllRequestedSeasons().includes(season.seasonNumber)
)
.map((season) => season.seasonNumber)
);
} else {
@ -108,7 +133,11 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
}
return (
selectedSeasons.length ===
data.seasons.filter((season) => season.seasonNumber !== 0).length
data.seasons
.filter((season) => season.seasonNumber !== 0)
.filter(
(season) => !getAllRequestedSeasons().includes(season.seasonNumber)
).length
);
};
@ -116,6 +145,23 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
? intl.formatMessage(messages.requestadmin)
: undefined;
const getSeasonRequest = (
seasonNumber: number
): SeasonRequest | undefined => {
let seasonRequest: SeasonRequest | undefined;
if (data?.mediaInfo && (data.mediaInfo.requests || []).length > 0) {
data.mediaInfo.requests.forEach((request) => {
if (!seasonRequest) {
seasonRequest = request.seasons.find(
(season) => season.seasonNumber === seasonNumber
);
}
});
}
return seasonRequest;
};
return (
<Modal
visible={visible}
@ -196,24 +242,34 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
<tbody className="bg-cool-gray-600 divide-y">
{data?.seasons
.filter((season) => season.seasonNumber !== 0)
.map((season) => (
.map((season) => {
const seasonRequest = getSeasonRequest(
season.seasonNumber
);
return (
<tr key={`season-${season.id}`}>
<td className="px-4 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-100">
<span
role="checkbox"
tabIndex={0}
aria-checked={isSelectedSeason(season.seasonNumber)}
aria-checked={
!!seasonRequest ||
isSelectedSeason(season.seasonNumber)
}
onClick={() => toggleSeason(season.seasonNumber)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === 'Space') {
toggleSeason(season.seasonNumber);
}
}}
className="group relative inline-flex items-center justify-center flex-shrink-0 h-5 w-10 cursor-pointer focus:outline-none"
className={`group relative inline-flex items-center justify-center flex-shrink-0 h-5 w-10 cursor-pointer focus:outline-none ${
seasonRequest ? 'opacity-50' : ''
}`}
>
<span
aria-hidden="true"
className={`${
!!seasonRequest ||
isSelectedSeason(season.seasonNumber)
? 'bg-indigo-500'
: 'bg-gray-800'
@ -222,6 +278,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
<span
aria-hidden="true"
className={`${
!!seasonRequest ||
isSelectedSeason(season.seasonNumber)
? 'translate-x-5'
: 'translate-x-0'
@ -238,12 +295,23 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
{season.episodeCount}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-200">
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-400 text-green-800">
Available
</span>
{!seasonRequest && <Badge>Not Requested</Badge>}
{seasonRequest?.status ===
MediaRequestStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
)}
{seasonRequest?.status ===
MediaRequestStatus.APPROVED && (
<Badge badgeType="danger">Unavailable</Badge>
)}
{seasonRequest?.status ===
MediaRequestStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
)}
</td>
</tr>
))}
);
})}
</tbody>
</table>
</div>

@ -6,7 +6,6 @@ import type { MediaStatus } from '../../../server/constants/media';
import TvRequestModal from './TvRequestModal';
interface RequestModalProps {
requestId?: number;
show: boolean;
type: 'movie' | 'tv';
tmdbId: number;
@ -18,7 +17,6 @@ interface RequestModalProps {
const RequestModal: React.FC<RequestModalProps> = ({
type,
requestId,
show,
tmdbId,
onComplete,
@ -26,16 +24,12 @@ const RequestModal: React.FC<RequestModalProps> = ({
onUpdating,
onCancel,
}) => {
const { data } = useSWR<MediaRequest>(
requestId ? `/api/v1/request/${requestId}` : null
);
if (type === 'tv') {
return (
<TvRequestModal
onComplete={onComplete}
onCancel={onCancel}
visible={show}
request={data}
tmdbId={tmdbId}
onUpdating={onUpdating}
/>
@ -47,7 +41,6 @@ const RequestModal: React.FC<RequestModalProps> = ({
onComplete={onComplete}
onCancel={onCancel}
visible={show}
request={data}
tmdbId={tmdbId}
onUpdating={onUpdating}
/>

@ -14,6 +14,7 @@ import { useUser, Permission } from '../../hooks/useUser';
import { TvDetails as TvDetailsType } from '../../../server/models/Tv';
import { MediaStatus } from '../../../server/constants/media';
import RequestModal from '../RequestModal';
import Badge from '../Common/Badge';
const messages = defineMessages({
userrating: 'User Rating',
@ -88,7 +89,6 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
tmdbId={data.id}
show={showRequestModal}
type="tv"
requestId={data.mediaInfo?.requests?.[0]?.id}
onComplete={() => {
revalidate();
setShowRequestModal(false);
@ -104,17 +104,31 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
/>
</div>
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
<span className="md:text-2xl md:leading-none">
{data.firstAirDate.slice(0, 4)}
</span>
<h1 className="text-2xl md:text-4xl">{data.name}</h1>
<div className="mb-2 md:mb-0">
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE && (
<Badge badgeType="success">Partially Available</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Badge badgeType="danger">Unavailable</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
)}
</div>
<h1 className="text-2xl md:text-4xl">
{data.name}{' '}
<span className="text-2xl">({data.firstAirDate.slice(0, 4)})</span>
</h1>
<span className="text-xs md:text-base mt-1 md:mt-0">
{data.genres.map((g) => g.name).join(', ')}
</span>
</div>
<div className="flex-1 flex justify-end mt-4 md:mt-0">
{(!data.mediaInfo ||
data.mediaInfo.status === MediaStatus.UNKNOWN) && (
data.mediaInfo.status !== MediaStatus.AVAILABLE) && (
<Button
buttonType="primary"
onClick={() => setShowRequestModal(true)}
@ -136,63 +150,6 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<FormattedMessage {...messages.request} />
</Button>
)}
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Button buttonType="warning">
<svg
className="w-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
<FormattedMessage {...messages.pending} />
</Button>
)}
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Button buttonType="danger">
<svg
className="w-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<FormattedMessage {...messages.unavailable} />
</Button>
)}
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Button buttonType="success">
<svg
className="w-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<FormattedMessage {...messages.available} />
</Button>
)}
<Button buttonType="danger" className="ml-2">
<svg
className="w-5"

Loading…
Cancel
Save