mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(frontend): approve/decline request well added to movie detail
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
|||||||
defineMessages,
|
defineMessages,
|
||||||
FormattedNumber,
|
FormattedNumber,
|
||||||
FormattedDate,
|
FormattedDate,
|
||||||
|
useIntl,
|
||||||
} from 'react-intl';
|
} from 'react-intl';
|
||||||
import type { MovieDetails as MovieDetailsType } from '../../../server/models/Movie';
|
import type { MovieDetails as MovieDetailsType } from '../../../server/models/Movie';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
@@ -20,6 +21,8 @@ import TitleCard from '../TitleCard';
|
|||||||
import PersonCard from '../PersonCard';
|
import PersonCard from '../PersonCard';
|
||||||
import { LanguageContext } from '../../context/LanguageContext';
|
import { LanguageContext } from '../../context/LanguageContext';
|
||||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||||
|
import { useUser, Permission } from '../../hooks/useUser';
|
||||||
|
import PendingRequest from '../PendingRequest';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
releasedate: 'Release Date',
|
releasedate: 'Release Date',
|
||||||
@@ -33,6 +36,12 @@ const messages = defineMessages({
|
|||||||
cast: 'Cast',
|
cast: 'Cast',
|
||||||
recommendations: 'Recommendations',
|
recommendations: 'Recommendations',
|
||||||
similar: 'Similar Titles',
|
similar: 'Similar Titles',
|
||||||
|
cancelrequest: 'Cancel Request',
|
||||||
|
available: 'Available',
|
||||||
|
unavailable: 'Unavailable',
|
||||||
|
request: 'Request',
|
||||||
|
pending: 'Pending',
|
||||||
|
overviewunavailable: 'Overview unavailable',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface MovieDetailsProps {
|
interface MovieDetailsProps {
|
||||||
@@ -54,7 +63,9 @@ enum MediaRequestStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||||
|
const { user, hasPermission } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const intl = useIntl();
|
||||||
const { locale } = useContext(LanguageContext);
|
const { locale } = useContext(LanguageContext);
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const [showRequestModal, setShowRequestModal] = useState(false);
|
const [showRequestModal, setShowRequestModal] = useState(false);
|
||||||
@@ -175,13 +186,16 @@ 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>
|
||||||
Request
|
<FormattedMessage {...messages.request} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{data.request?.status === MediaRequestStatus.PENDING && (
|
{data.request?.status === MediaRequestStatus.PENDING && (
|
||||||
<Button
|
<Button
|
||||||
buttonType="warning"
|
buttonType="warning"
|
||||||
onClick={() => setShowCancelModal(true)}
|
onClick={() => {
|
||||||
|
if (data.request?.requestedBy.id === user?.id)
|
||||||
|
setShowCancelModal(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-4 mr-2"
|
className="w-4 mr-2"
|
||||||
@@ -197,7 +211,9 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
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"
|
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>
|
</svg>
|
||||||
Pending
|
{data.request?.requestedBy.id === user?.id
|
||||||
|
? intl.formatMessage(messages.cancelrequest)
|
||||||
|
: intl.formatMessage(messages.pending)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{data.request?.status === MediaRequestStatus.APPROVED && (
|
{data.request?.status === MediaRequestStatus.APPROVED && (
|
||||||
@@ -216,7 +232,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Unavailable
|
<FormattedMessage {...messages.unavailable} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{data.request?.status === MediaRequestStatus.AVAILABLE && (
|
{data.request?.status === MediaRequestStatus.AVAILABLE && (
|
||||||
@@ -235,7 +251,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
d="M5 13l4 4L19 7"
|
d="M5 13l4 4L19 7"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Available
|
<FormattedMessage {...messages.available} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button buttonType="danger" className="ml-2">
|
<Button buttonType="danger" className="ml-2">
|
||||||
@@ -255,37 +271,50 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
<Button buttonType="default" className="ml-2">
|
{hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
<svg
|
<Button buttonType="default" className="ml-2">
|
||||||
className="w-5"
|
<svg
|
||||||
style={{ height: 20 }}
|
className="w-5"
|
||||||
fill="none"
|
style={{ height: 20 }}
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
stroke="currentColor"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 24 24"
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path
|
>
|
||||||
strokeLinecap="round"
|
<path
|
||||||
strokeLinejoin="round"
|
strokeLinecap="round"
|
||||||
strokeWidth={2}
|
strokeLinejoin="round"
|
||||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
strokeWidth={2}
|
||||||
/>
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||||
<path
|
/>
|
||||||
strokeLinecap="round"
|
<path
|
||||||
strokeLinejoin="round"
|
strokeLinecap="round"
|
||||||
strokeWidth={2}
|
strokeLinejoin="round"
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
strokeWidth={2}
|
||||||
/>
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
</svg>
|
/>
|
||||||
</Button>
|
</svg>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex pt-8 text-white flex-col md:flex-row pb-4">
|
<div className="flex pt-8 text-white flex-col md:flex-row pb-4">
|
||||||
<div className="flex-1 md:mr-8">
|
<div className="flex-1 md:mr-8">
|
||||||
|
{data.request?.status === MediaRequestStatus.PENDING &&
|
||||||
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
|
<PendingRequest
|
||||||
|
request={data.request}
|
||||||
|
onUpdate={() => revalidate()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<h2 className="text-xl md:text-2xl">
|
<h2 className="text-xl md:text-2xl">
|
||||||
<FormattedMessage {...messages.overview} />
|
<FormattedMessage {...messages.overview} />
|
||||||
</h2>
|
</h2>
|
||||||
<p className="pt-2 text-sm md:text-base">{data.overview}</p>
|
<p className="pt-2 text-sm md:text-base">
|
||||||
|
{data.overview
|
||||||
|
? data.overview
|
||||||
|
: intl.formatMessage(messages.overviewunavailable)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:w-80 mt-8 md:mt-0">
|
<div className="w-full md:w-80 mt-8 md:mt-0">
|
||||||
<div className="bg-cool-gray-900 rounded-lg shadow border border-cool-gray-800">
|
<div className="bg-cool-gray-900 rounded-lg shadow border border-cool-gray-800">
|
||||||
|
108
src/components/PendingRequest/index.tsx
Normal file
108
src/components/PendingRequest/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||||
|
import Button from '../Common/Button';
|
||||||
|
import { MediaRequest } from '../../../server/entity/MediaRequest';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
pendingtitle: 'Pending Request',
|
||||||
|
pendingdescription:
|
||||||
|
'This title was requested by {username} ({email}) on {date}',
|
||||||
|
approve: 'Approve',
|
||||||
|
decline: 'Decline',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PendingRequestProps {
|
||||||
|
request: MediaRequest;
|
||||||
|
onUpdate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PendingRequest: React.FC<PendingRequestProps> = ({
|
||||||
|
request,
|
||||||
|
onUpdate,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [isLoading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const updateStatus = async (status: 'approve' | 'decline') => {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.get(`/api/v1/request/${request.id}/${status}`);
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
onUpdate();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-cool-gray-900 border border-cool-gray-800 sm:rounded-lg mb-6 shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-cool-gray-100">
|
||||||
|
<FormattedMessage {...messages.pendingtitle} />
|
||||||
|
</h3>
|
||||||
|
<div className="mt-2 max-w-xl text-sm leading-5 text-cool-gray-400">
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.pendingdescription}
|
||||||
|
values={{
|
||||||
|
username: request.requestedBy.username,
|
||||||
|
email: request.requestedBy.email,
|
||||||
|
date: intl.formatDate(request.updatedAt),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<span className="inline-flex rounded-md shadow-sm mr-2">
|
||||||
|
<Button
|
||||||
|
buttonType="success"
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={() => updateStatus('approve')}
|
||||||
|
>
|
||||||
|
<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.approve} />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="danger"
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={() => updateStatus('decline')}
|
||||||
|
>
|
||||||
|
<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="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<FormattedMessage {...messages.decline} />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PendingRequest;
|
@@ -9,17 +9,27 @@
|
|||||||
"components.Layout.Sidebar.dashboard": "Dashboard",
|
"components.Layout.Sidebar.dashboard": "Dashboard",
|
||||||
"components.Layout.Sidebar.requests": "Requests",
|
"components.Layout.Sidebar.requests": "Requests",
|
||||||
"components.Layout.Sidebar.settings": "Settings",
|
"components.Layout.Sidebar.settings": "Settings",
|
||||||
|
"components.MovieDetails.available": "Available",
|
||||||
"components.MovieDetails.budget": "Budget",
|
"components.MovieDetails.budget": "Budget",
|
||||||
|
"components.MovieDetails.cancelrequest": "Cancel Request",
|
||||||
"components.MovieDetails.cast": "Cast",
|
"components.MovieDetails.cast": "Cast",
|
||||||
"components.MovieDetails.originallanguage": "Original Language",
|
"components.MovieDetails.originallanguage": "Original Language",
|
||||||
"components.MovieDetails.overview": "Overview",
|
"components.MovieDetails.overview": "Overview",
|
||||||
|
"components.MovieDetails.overviewunavailable": "Overview unavailable",
|
||||||
|
"components.MovieDetails.pending": "Pending",
|
||||||
"components.MovieDetails.recommendations": "Recommendations",
|
"components.MovieDetails.recommendations": "Recommendations",
|
||||||
"components.MovieDetails.releasedate": "Release Date",
|
"components.MovieDetails.releasedate": "Release Date",
|
||||||
|
"components.MovieDetails.request": "Request",
|
||||||
"components.MovieDetails.revenue": "Revenue",
|
"components.MovieDetails.revenue": "Revenue",
|
||||||
"components.MovieDetails.runtime": "{minutes} minutes",
|
"components.MovieDetails.runtime": "{minutes} minutes",
|
||||||
"components.MovieDetails.similar": "Similar Titles",
|
"components.MovieDetails.similar": "Similar Titles",
|
||||||
"components.MovieDetails.status": "Status",
|
"components.MovieDetails.status": "Status",
|
||||||
|
"components.MovieDetails.unavailable": "Unavailable",
|
||||||
"components.MovieDetails.userrating": "User Rating",
|
"components.MovieDetails.userrating": "User Rating",
|
||||||
|
"components.PendingRequest.approve": "Approve",
|
||||||
|
"components.PendingRequest.decline": "Decline",
|
||||||
|
"components.PendingRequest.pendingdescription": "This title was requested by {username} ({email}) on {date}",
|
||||||
|
"components.PendingRequest.pendingtitle": "Pending Request",
|
||||||
"components.RequestModal.cancelrequest": "This will remove your request. Are you sure you want to continue?",
|
"components.RequestModal.cancelrequest": "This will remove your request. Are you sure you want to continue?",
|
||||||
"components.RequestModal.requestadmin": "Your request will be immediately approved. Do you wish to continue?"
|
"components.RequestModal.requestadmin": "Your request will be immediately approved. Do you wish to continue?"
|
||||||
}
|
}
|
||||||
|
@@ -9,17 +9,27 @@
|
|||||||
"components.Layout.Sidebar.dashboard": "ホーム",
|
"components.Layout.Sidebar.dashboard": "ホーム",
|
||||||
"components.Layout.Sidebar.requests": "リクエスト",
|
"components.Layout.Sidebar.requests": "リクエスト",
|
||||||
"components.Layout.Sidebar.settings": "設定",
|
"components.Layout.Sidebar.settings": "設定",
|
||||||
|
"components.MovieDetails.available": "",
|
||||||
"components.MovieDetails.budget": "興行収入",
|
"components.MovieDetails.budget": "興行収入",
|
||||||
|
"components.MovieDetails.cancelrequest": "チャンセルリクエスト",
|
||||||
"components.MovieDetails.cast": "キャスト",
|
"components.MovieDetails.cast": "キャスト",
|
||||||
"components.MovieDetails.originallanguage": "言語",
|
"components.MovieDetails.originallanguage": "言語",
|
||||||
"components.MovieDetails.overview": "ストーリー",
|
"components.MovieDetails.overview": "ストーリー",
|
||||||
|
"components.MovieDetails.overviewunavailable": "",
|
||||||
|
"components.MovieDetails.pending": "リクエスト中",
|
||||||
"components.MovieDetails.recommendations": "オススメの作品",
|
"components.MovieDetails.recommendations": "オススメの作品",
|
||||||
"components.MovieDetails.releasedate": "公開日",
|
"components.MovieDetails.releasedate": "公開日",
|
||||||
|
"components.MovieDetails.request": "リクエストする",
|
||||||
"components.MovieDetails.revenue": "製作費",
|
"components.MovieDetails.revenue": "製作費",
|
||||||
"components.MovieDetails.runtime": "{minutes}分",
|
"components.MovieDetails.runtime": "{minutes}分",
|
||||||
"components.MovieDetails.similar": "関連作品",
|
"components.MovieDetails.similar": "関連作品",
|
||||||
"components.MovieDetails.status": "状態",
|
"components.MovieDetails.status": "状態",
|
||||||
|
"components.MovieDetails.unavailable": "",
|
||||||
"components.MovieDetails.userrating": "ユーザー評価",
|
"components.MovieDetails.userrating": "ユーザー評価",
|
||||||
|
"components.PendingRequest.approve": "",
|
||||||
|
"components.PendingRequest.decline": "",
|
||||||
|
"components.PendingRequest.pendingdescription": "",
|
||||||
|
"components.PendingRequest.pendingtitle": "",
|
||||||
"components.RequestModal.cancelrequest": "このリクエストをキャンセルしてよろしいですか?",
|
"components.RequestModal.cancelrequest": "このリクエストをキャンセルしてよろしいですか?",
|
||||||
"components.RequestModal.requestadmin": "このリクエストが今すぐ承認致します。よろしいですか?"
|
"components.RequestModal.requestadmin": "このリクエストが今すぐ承認致します。よろしいですか?"
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user