mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(frontend): cancel movie request modal
also includes tons of performance fixes for the modals
This commit is contained in:
1
src/assets/download.svg
Normal file
1
src/assets/download.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
After Width: | Height: | Size: 326 B |
@@ -14,10 +14,10 @@ const Badge: React.FC<BadgeProps> = ({ badgeType = 'default', children }) => {
|
|||||||
badgeStyle.push('bg-red-600 text-red-100');
|
badgeStyle.push('bg-red-600 text-red-100');
|
||||||
break;
|
break;
|
||||||
case 'warning':
|
case 'warning':
|
||||||
badgeStyle.push('bg-orange-400 text-orange-100');
|
badgeStyle.push('bg-orange-500 text-orange-100');
|
||||||
break;
|
break;
|
||||||
case 'success':
|
case 'success':
|
||||||
badgeStyle.push('bg-green-500 text-green-100');
|
badgeStyle.push('bg-green-400 text-green-100');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
badgeStyle.push('bg-indigo-500 text-indigo-100');
|
badgeStyle.push('bg-indigo-500 text-indigo-100');
|
||||||
|
@@ -6,16 +6,23 @@ import { useLockBodyScroll } from '../../../hooks/useLockBodyScroll';
|
|||||||
import LoadingSpinner from '../LoadingSpinner';
|
import LoadingSpinner from '../LoadingSpinner';
|
||||||
import useClickOutside from '../../../hooks/useClickOutside';
|
import useClickOutside from '../../../hooks/useClickOutside';
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
title?: string;
|
title?: string;
|
||||||
onCancel?: (e?: MouseEvent<HTMLElement>) => void;
|
onCancel?: (e?: MouseEvent<HTMLElement>) => void;
|
||||||
onOk?: (e: MouseEvent<HTMLButtonElement>) => void;
|
onOk?: (e?: MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
onSecondary?: (e?: MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
onTertiary?: (e?: MouseEvent<HTMLButtonElement>) => void;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
okText?: string;
|
okText?: string;
|
||||||
|
secondaryText?: string;
|
||||||
|
tertiaryText?: string;
|
||||||
okDisabled?: boolean;
|
okDisabled?: boolean;
|
||||||
cancelButtonType?: ButtonType;
|
cancelButtonType?: ButtonType;
|
||||||
okButtonType?: ButtonType;
|
okButtonType?: ButtonType;
|
||||||
visible?: boolean;
|
secondaryButtonType?: ButtonType;
|
||||||
|
secondaryDisabled?: boolean;
|
||||||
|
tertiaryDisabled?: boolean;
|
||||||
|
tertiaryButtonType?: ButtonType;
|
||||||
disableScrollLock?: boolean;
|
disableScrollLock?: boolean;
|
||||||
backgroundClickable?: boolean;
|
backgroundClickable?: boolean;
|
||||||
iconSvg?: ReactNode;
|
iconSvg?: ReactNode;
|
||||||
@@ -29,14 +36,22 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
cancelText,
|
cancelText,
|
||||||
okText,
|
okText,
|
||||||
okDisabled = false,
|
okDisabled = false,
|
||||||
cancelButtonType,
|
cancelButtonType = 'default',
|
||||||
okButtonType,
|
okButtonType = 'primary',
|
||||||
children,
|
children,
|
||||||
visible,
|
|
||||||
disableScrollLock,
|
disableScrollLock,
|
||||||
backgroundClickable = true,
|
backgroundClickable = true,
|
||||||
iconSvg,
|
iconSvg,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
secondaryButtonType = 'default',
|
||||||
|
secondaryDisabled = false,
|
||||||
|
onSecondary,
|
||||||
|
secondaryText,
|
||||||
|
tertiaryButtonType = 'default',
|
||||||
|
tertiaryDisabled = false,
|
||||||
|
tertiaryText,
|
||||||
|
onTertiary,
|
||||||
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
useClickOutside(modalRef, () => {
|
useClickOutside(modalRef, () => {
|
||||||
@@ -44,39 +59,26 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
? onCancel()
|
? onCancel()
|
||||||
: undefined;
|
: undefined;
|
||||||
});
|
});
|
||||||
useLockBodyScroll(!!visible, disableScrollLock);
|
useLockBodyScroll(true, disableScrollLock);
|
||||||
const transitions = useTransition(visible, null, {
|
const containerTransitions = useTransition(!loading, null, {
|
||||||
from: { opacity: 0, backdropFilter: 'blur(0px)' },
|
|
||||||
enter: { opacity: 1, backdropFilter: 'blur(3px)' },
|
|
||||||
leave: { opacity: 0, backdropFilter: 'blur(0px)' },
|
|
||||||
config: { tension: 500, velocity: 40, friction: 60 },
|
|
||||||
});
|
|
||||||
const containerTransitions = useTransition(visible && !loading, null, {
|
|
||||||
from: { opacity: 0, transform: 'scale(0.5)' },
|
from: { opacity: 0, transform: 'scale(0.5)' },
|
||||||
enter: { opacity: 1, transform: 'scale(1)' },
|
enter: { opacity: 1, transform: 'scale(1)' },
|
||||||
leave: { opacity: 0, transform: 'scale(0.5)' },
|
leave: { opacity: 0, transform: 'scale(0.5)' },
|
||||||
config: { tension: 500, velocity: 40, friction: 60 },
|
config: { tension: 500, velocity: 40, friction: 60 },
|
||||||
});
|
});
|
||||||
const loadingTransitions = useTransition(visible && loading, null, {
|
const loadingTransitions = useTransition(loading, null, {
|
||||||
from: { opacity: 0, transform: 'scale(0.5)' },
|
from: { opacity: 0, transform: 'scale(0.5)' },
|
||||||
enter: { opacity: 1, transform: 'scale(1)' },
|
enter: { opacity: 1, transform: 'scale(1)' },
|
||||||
leave: { opacity: 0, transform: 'scale(0.5)' },
|
leave: { opacity: 0, transform: 'scale(0.5)' },
|
||||||
config: { tension: 500, velocity: 40, friction: 60 },
|
config: { tension: 500, velocity: 40, friction: 60 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancelType = cancelButtonType ?? 'default';
|
|
||||||
const okType = okButtonType ?? 'primary';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{transitions.map(
|
{ReactDOM.createPortal(
|
||||||
({ props, item, key }) =>
|
|
||||||
item &&
|
|
||||||
ReactDOM.createPortal(
|
|
||||||
<animated.div
|
<animated.div
|
||||||
className="fixed top-0 left-0 right-0 bottom-0 bg-cool-gray-800 bg-opacity-50 w-full h-full z-50 flex justify-center items-center"
|
className="fixed top-0 left-0 right-0 bottom-0 bg-cool-gray-800 bg-opacity-50 w-full h-full z-50 flex justify-center items-center"
|
||||||
style={props}
|
style={props.style}
|
||||||
key={key}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
typeof onCancel === 'function' && backgroundClickable
|
typeof onCancel === 'function' && backgroundClickable
|
||||||
@@ -132,11 +134,11 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(onCancel || onOk) && (
|
{(onCancel || onOk || onSecondary || onTertiary) && (
|
||||||
<div className="mt-5 sm:mt-4 flex justify-center sm:justify-start flex-row-reverse">
|
<div className="mt-5 sm:mt-4 flex justify-center sm:justify-start flex-row-reverse">
|
||||||
{typeof onOk === 'function' && (
|
{typeof onOk === 'function' && (
|
||||||
<Button
|
<Button
|
||||||
buttonType={okType}
|
buttonType={okButtonType}
|
||||||
onClick={onOk}
|
onClick={onOk}
|
||||||
className="ml-3"
|
className="ml-3"
|
||||||
disabled={okDisabled}
|
disabled={okDisabled}
|
||||||
@@ -144,9 +146,29 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
{okText ? okText : 'Ok'}
|
{okText ? okText : 'Ok'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{typeof onSecondary === 'function' && secondaryText && (
|
||||||
|
<Button
|
||||||
|
buttonType={secondaryButtonType}
|
||||||
|
onClick={onSecondary}
|
||||||
|
className="ml-3"
|
||||||
|
disabled={secondaryDisabled}
|
||||||
|
>
|
||||||
|
{secondaryText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{typeof onTertiary === 'function' && tertiaryText && (
|
||||||
|
<Button
|
||||||
|
buttonType={tertiaryButtonType}
|
||||||
|
onClick={onTertiary}
|
||||||
|
className="ml-3"
|
||||||
|
disabled={tertiaryDisabled}
|
||||||
|
>
|
||||||
|
{tertiaryText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{typeof onCancel === 'function' && (
|
{typeof onCancel === 'function' && (
|
||||||
<Button
|
<Button
|
||||||
buttonType={cancelType}
|
buttonType={cancelButtonType}
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="ml-3 sm:ml-0 sm:px-4"
|
className="ml-3 sm:ml-0 sm:px-4"
|
||||||
>
|
>
|
||||||
@@ -160,7 +182,6 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
)}
|
)}
|
||||||
</animated.div>,
|
</animated.div>,
|
||||||
document.body
|
document.body
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@@ -38,6 +38,7 @@ const messages = defineMessages({
|
|||||||
available: 'Available',
|
available: 'Available',
|
||||||
unavailable: 'Unavailable',
|
unavailable: 'Unavailable',
|
||||||
request: 'Request',
|
request: 'Request',
|
||||||
|
viewrequest: 'View Request',
|
||||||
pending: 'Pending',
|
pending: 'Pending',
|
||||||
overviewunavailable: 'Overview unavailable',
|
overviewunavailable: 'Overview unavailable',
|
||||||
});
|
});
|
||||||
@@ -53,13 +54,6 @@ interface SearchResult {
|
|||||||
results: MovieResult[];
|
results: MovieResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MediaRequestStatus {
|
|
||||||
PENDING = 1,
|
|
||||||
APPROVED,
|
|
||||||
DECLINED,
|
|
||||||
AVAILABLE,
|
|
||||||
}
|
|
||||||
|
|
||||||
const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||||
const { hasPermission } = useUser();
|
const { hasPermission } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -87,6 +81,11 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
return <div>Broken?</div>;
|
return <div>Broken?</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(MediaStatus);
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
const activeRequest = data?.mediaInfo?.requests?.[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="bg-cover bg-center -mx-4 -mt-2 px-4 sm:px-8 pt-4 "
|
className="bg-cover bg-center -mx-4 -mt-2 px-4 sm:px-8 pt-4 "
|
||||||
@@ -144,11 +143,26 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex justify-end mt-4 md:mt-0">
|
<div className="flex-1 flex justify-end mt-4 md:mt-0">
|
||||||
{(!data.mediaInfo ||
|
{(!data.mediaInfo ||
|
||||||
data.mediaInfo?.status === MediaStatus.UNKNOWN) && (
|
data.mediaInfo?.status === MediaStatus.UNKNOWN ||
|
||||||
|
activeRequest) && (
|
||||||
<Button
|
<Button
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
onClick={() => setShowRequestModal(true)}
|
onClick={() => setShowRequestModal(true)}
|
||||||
>
|
>
|
||||||
|
{activeRequest ? (
|
||||||
|
<svg
|
||||||
|
className="w-4 mr-1"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
<svg
|
<svg
|
||||||
className="w-4 mr-1"
|
className="w-4 mr-1"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -163,83 +177,12 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<FormattedMessage {...messages.request} />
|
)}
|
||||||
|
<FormattedMessage
|
||||||
|
{...(activeRequest ? messages.viewrequest : messages.request)}
|
||||||
|
/>
|
||||||
</Button>
|
</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"
|
|
||||||
style={{ height: 20 }}
|
|
||||||
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 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
{hasPermission(Permission.MANAGE_REQUESTS) && (
|
{hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
<Button buttonType="default" className="ml-2">
|
<Button buttonType="default" className="ml-2">
|
||||||
<svg
|
<svg
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import Modal from '../Common/Modal';
|
import Modal from '../Common/Modal';
|
||||||
import { useUser } from '../../hooks/useUser';
|
import { useUser } from '../../hooks/useUser';
|
||||||
import { Permission } from '../../../server/lib/permissions';
|
import { Permission } from '../../../server/lib/permissions';
|
||||||
@@ -8,7 +8,11 @@ import useSWR from 'swr';
|
|||||||
import { MovieDetails } from '../../../server/models/Movie';
|
import { MovieDetails } from '../../../server/models/Movie';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { MediaStatus } from '../../../server/constants/media';
|
import {
|
||||||
|
MediaStatus,
|
||||||
|
MediaRequestStatus,
|
||||||
|
} from '../../../server/constants/media';
|
||||||
|
import DownloadIcon from '../../assets/download.svg';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
requestadmin:
|
requestadmin:
|
||||||
@@ -17,29 +21,28 @@ const messages = defineMessages({
|
|||||||
'This will remove your request. Are you sure you want to continue?',
|
'This will remove your request. Are you sure you want to continue?',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface RequestModalProps {
|
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
request?: MediaRequest;
|
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
visible?: boolean;
|
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onComplete?: (newStatus: MediaStatus) => void;
|
onComplete?: (newStatus: MediaStatus) => void;
|
||||||
onUpdating?: (isUpdating: boolean) => void;
|
onUpdating?: (isUpdating: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MovieRequestModal: React.FC<RequestModalProps> = ({
|
const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||||
visible,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
onComplete,
|
onComplete,
|
||||||
request,
|
|
||||||
tmdbId,
|
tmdbId,
|
||||||
onUpdating,
|
onUpdating,
|
||||||
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const { data, error } = useSWR<MovieDetails>(`/api/v1/movie/${tmdbId}`);
|
const { data, error } = useSWR<MovieDetails>(`/api/v1/movie/${tmdbId}`, {
|
||||||
|
revalidateOnMount: true,
|
||||||
|
});
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { hasPermission } = useUser();
|
const { user, hasPermission } = useUser();
|
||||||
|
|
||||||
const sendRequest = async () => {
|
const sendRequest = useCallback(async () => {
|
||||||
if (onUpdating) {
|
if (onUpdating) {
|
||||||
onUpdating(true);
|
onUpdating(true);
|
||||||
}
|
}
|
||||||
@@ -62,59 +65,76 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
|||||||
onUpdating(false);
|
onUpdating(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [data, onComplete, onUpdating, addToast]);
|
||||||
|
|
||||||
|
const activeRequest = data?.mediaInfo?.requests?.[0];
|
||||||
|
|
||||||
|
console.log(activeRequest);
|
||||||
|
|
||||||
|
const cancelRequest = async () => {
|
||||||
|
if (onUpdating) {
|
||||||
|
onUpdating(true);
|
||||||
|
}
|
||||||
|
const response = await axios.delete<MediaRequest>(
|
||||||
|
`/api/v1/request/${activeRequest?.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete(MediaStatus.UNKNOWN);
|
||||||
|
}
|
||||||
|
addToast(
|
||||||
|
<span>
|
||||||
|
<strong>{data?.title}</strong> request cancelled!
|
||||||
|
</span>,
|
||||||
|
{ appearance: 'success', autoDismiss: true }
|
||||||
|
);
|
||||||
|
if (onUpdating) {
|
||||||
|
onUpdating(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = hasPermission(Permission.MANAGE_REQUESTS)
|
const isOwner = activeRequest
|
||||||
|
? activeRequest.requestedBy.id === user?.id ||
|
||||||
|
hasPermission(Permission.MANAGE_REQUESTS)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const text = hasPermission(Permission.MANAGE_REQUESTS)
|
||||||
? intl.formatMessage(messages.requestadmin)
|
? intl.formatMessage(messages.requestadmin)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (request) {
|
if (activeRequest?.status === MediaRequestStatus.PENDING) {
|
||||||
text = intl.formatMessage(messages.cancelrequest);
|
return (
|
||||||
|
<Modal
|
||||||
|
loading={!data && !error}
|
||||||
|
backgroundClickable
|
||||||
|
onCancel={onCancel}
|
||||||
|
onOk={isOwner ? () => cancelRequest() : undefined}
|
||||||
|
title={`Pending request for ${data?.title}`}
|
||||||
|
okText={'Cancel Request'}
|
||||||
|
okButtonType={'danger'}
|
||||||
|
cancelText="Close"
|
||||||
|
iconSvg={<DownloadIcon className="w-6 h-6" />}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
There is currently a pending request from{' '}
|
||||||
|
<strong>{activeRequest.requestedBy.username}</strong>.
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
|
||||||
loading={!data && !error}
|
loading={!data && !error}
|
||||||
backgroundClickable
|
backgroundClickable
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onOk={() => sendRequest()}
|
onOk={sendRequest}
|
||||||
title={!request ? `Request ${data?.title}` : 'Cancel Request'}
|
title={`Request ${data?.title}`}
|
||||||
okText={!request ? 'Request' : 'Cancel Request'}
|
okText={'Request'}
|
||||||
okButtonType={!!request ? 'danger' : 'primary'}
|
okButtonType={'primary'}
|
||||||
iconSvg={
|
iconSvg={<DownloadIcon className="w-6 h-6" />}
|
||||||
!request ? (
|
{...props}
|
||||||
<svg
|
|
||||||
className="w-6 h-6"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg
|
|
||||||
className="w-6 h-6"
|
|
||||||
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 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@@ -21,27 +21,22 @@ const messages = defineMessages({
|
|||||||
'This will remove your request. Are you sure you want to continue?',
|
'This will remove your request. Are you sure you want to continue?',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface RequestModalProps {
|
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
request?: MediaRequest;
|
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
visible?: boolean;
|
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onComplete?: (newStatus: MediaStatus) => void;
|
onComplete?: (newStatus: MediaStatus) => void;
|
||||||
onUpdating?: (isUpdating: boolean) => void;
|
onUpdating?: (isUpdating: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TvRequestModal: React.FC<RequestModalProps> = ({
|
const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||||
visible,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
onComplete,
|
onComplete,
|
||||||
request,
|
|
||||||
tmdbId,
|
tmdbId,
|
||||||
onUpdating,
|
onUpdating,
|
||||||
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const { data, error } = useSWR<TvDetails>(
|
const { data, error } = useSWR<TvDetails>(`/api/v1/tv/${tmdbId}`);
|
||||||
visible ? `/api/v1/tv/${tmdbId}` : null
|
|
||||||
);
|
|
||||||
const [selectedSeasons, setSelectedSeasons] = useState<number[]>([]);
|
const [selectedSeasons, setSelectedSeasons] = useState<number[]>([]);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { hasPermission } = useUser();
|
const { hasPermission } = useUser();
|
||||||
@@ -164,7 +159,6 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
|
||||||
loading={!data && !error}
|
loading={!data && !error}
|
||||||
backgroundClickable
|
backgroundClickable
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
@@ -193,6 +187,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="-mx-4 sm:mx-0 overflow-auto max-h-96">
|
<div className="-mx-4 sm:mx-0 overflow-auto max-h-96">
|
||||||
|
@@ -4,6 +4,7 @@ import MovieRequestModal from './MovieRequestModal';
|
|||||||
import type { MediaRequest } from '../../../server/entity/MediaRequest';
|
import type { MediaRequest } from '../../../server/entity/MediaRequest';
|
||||||
import type { MediaStatus } from '../../../server/constants/media';
|
import type { MediaStatus } from '../../../server/constants/media';
|
||||||
import TvRequestModal from './TvRequestModal';
|
import TvRequestModal from './TvRequestModal';
|
||||||
|
import { useTransition, animated } from 'react-spring';
|
||||||
|
|
||||||
interface RequestModalProps {
|
interface RequestModalProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -24,26 +25,49 @@ const RequestModal: React.FC<RequestModalProps> = ({
|
|||||||
onUpdating,
|
onUpdating,
|
||||||
onCancel,
|
onCancel,
|
||||||
}) => {
|
}) => {
|
||||||
|
const transitions = useTransition(show, null, {
|
||||||
|
from: { opacity: 0, backdropFilter: 'blur(0px)' },
|
||||||
|
enter: { opacity: 1, backdropFilter: 'blur(3px)' },
|
||||||
|
leave: { opacity: 0, backdropFilter: 'blur(0px)' },
|
||||||
|
config: { tension: 500, velocity: 40, friction: 60 },
|
||||||
|
});
|
||||||
|
|
||||||
if (type === 'tv') {
|
if (type === 'tv') {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{transitions.map(
|
||||||
|
({ props, item, key }) =>
|
||||||
|
item && (
|
||||||
<TvRequestModal
|
<TvRequestModal
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
visible={show}
|
|
||||||
tmdbId={tmdbId}
|
tmdbId={tmdbId}
|
||||||
onUpdating={onUpdating}
|
onUpdating={onUpdating}
|
||||||
|
style={props}
|
||||||
|
key={key}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{transitions.map(
|
||||||
|
({ props, item, key }) =>
|
||||||
|
item && (
|
||||||
<MovieRequestModal
|
<MovieRequestModal
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
visible={show}
|
|
||||||
tmdbId={tmdbId}
|
tmdbId={tmdbId}
|
||||||
onUpdating={onUpdating}
|
onUpdating={onUpdating}
|
||||||
|
style={props}
|
||||||
|
key={key}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import type { MediaType } from '../../../server/models/Search';
|
import type { MediaType } from '../../../server/models/Search';
|
||||||
import Available from '../../assets/available.svg';
|
import Available from '../../assets/available.svg';
|
||||||
import Requested from '../../assets/requested.svg';
|
import Requested from '../../assets/requested.svg';
|
||||||
@@ -42,18 +42,27 @@ const TitleCard: React.FC<TitleCardProps> = ({
|
|||||||
year = year.slice(0, 4);
|
year = year.slice(0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestComplete = useCallback((newStatus: MediaStatus) => {
|
||||||
|
setCurrentStatus(newStatus);
|
||||||
|
setShowRequestModal(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const requestUpdating = useCallback(
|
||||||
|
(status: boolean) => setIsUpdating(status),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeModal = useCallback(() => setShowRequestModal(false), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-36 sm:w-36 md:w-44">
|
<div className="w-36 sm:w-36 md:w-44">
|
||||||
<RequestModal
|
<RequestModal
|
||||||
tmdbId={id}
|
tmdbId={id}
|
||||||
show={showRequestModal}
|
show={showRequestModal}
|
||||||
type={mediaType === 'movie' ? 'movie' : 'tv'}
|
type={mediaType === 'movie' ? 'movie' : 'tv'}
|
||||||
onComplete={(newStatus) => {
|
onComplete={requestComplete}
|
||||||
setCurrentStatus(newStatus);
|
onUpdating={requestUpdating}
|
||||||
setShowRequestModal(false);
|
onCancel={closeModal}
|
||||||
}}
|
|
||||||
onUpdating={(status) => setIsUpdating(status)}
|
|
||||||
onCancel={() => setShowRequestModal(false)}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="titleCard outline-none cursor-default"
|
className="titleCard outline-none cursor-default"
|
||||||
|
@@ -150,23 +150,6 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
|||||||
<FormattedMessage {...messages.request} />
|
<FormattedMessage {...messages.request} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button buttonType="danger" className="ml-2">
|
|
||||||
<svg
|
|
||||||
className="w-5"
|
|
||||||
style={{ height: 20 }}
|
|
||||||
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 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
{hasPermission(Permission.MANAGE_REQUESTS) && (
|
{hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
<Button buttonType="default" className="ml-2">
|
<Button buttonType="default" className="ml-2">
|
||||||
<svg
|
<svg
|
||||||
|
Reference in New Issue
Block a user