You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
304 lines
11 KiB
304 lines
11 KiB
import { RadioGroup } from '@headlessui/react';
|
|
import { ExclamationIcon } from '@heroicons/react/outline';
|
|
import { ArrowCircleRightIcon } from '@heroicons/react/solid';
|
|
import axios from 'axios';
|
|
import { Field, Formik } from 'formik';
|
|
import Link from 'next/link';
|
|
import React from 'react';
|
|
import { defineMessages, useIntl } from 'react-intl';
|
|
import { useToasts } from 'react-toast-notifications';
|
|
import useSWR from 'swr';
|
|
import * as Yup from 'yup';
|
|
import type Issue from '../../../../server/entity/Issue';
|
|
import { MovieDetails } from '../../../../server/models/Movie';
|
|
import { TvDetails } from '../../../../server/models/Tv';
|
|
import globalMessages from '../../../i18n/globalMessages';
|
|
import Button from '../../Common/Button';
|
|
import Modal from '../../Common/Modal';
|
|
import { issueOptions } from '../constants';
|
|
|
|
const messages = defineMessages({
|
|
validationMessageRequired: 'You must provide a description',
|
|
issomethingwrong: 'Is there a problem with {title}?',
|
|
whatswrong: "What's wrong?",
|
|
providedetail: 'Provide a detailed explanation of the issue.',
|
|
season: 'Season {seasonNumber}',
|
|
episode: 'Episode {episodeNumber}',
|
|
allseasons: 'All Seasons',
|
|
allepisodes: 'All Episodes',
|
|
problemseason: 'Affected Season',
|
|
problemepisode: 'Affected Episode',
|
|
toastSuccessCreate:
|
|
'Issue report for <strong>{title}</strong> submitted successfully!',
|
|
toastFailedCreate: 'Something went wrong while submitting the issue.',
|
|
toastviewissue: 'View Issue',
|
|
reportissue: 'Report an Issue',
|
|
submitissue: 'Submit Issue',
|
|
});
|
|
|
|
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
|
return (movie as MovieDetails).title !== undefined;
|
|
};
|
|
|
|
const classNames = (...classes: string[]) => {
|
|
return classes.filter(Boolean).join(' ');
|
|
};
|
|
|
|
interface CreateIssueModalProps {
|
|
mediaType: 'movie' | 'tv';
|
|
tmdbId?: number;
|
|
onCancel?: () => void;
|
|
}
|
|
|
|
const CreateIssueModal: React.FC<CreateIssueModalProps> = ({
|
|
onCancel,
|
|
mediaType,
|
|
tmdbId,
|
|
}) => {
|
|
const intl = useIntl();
|
|
const { addToast } = useToasts();
|
|
const { data, error } = useSWR<MovieDetails | TvDetails>(
|
|
tmdbId ? `/api/v1/${mediaType}/${tmdbId}` : null
|
|
);
|
|
|
|
if (!tmdbId) {
|
|
return null;
|
|
}
|
|
|
|
const CreateIssueModalSchema = Yup.object().shape({
|
|
message: Yup.string().required(
|
|
intl.formatMessage(messages.validationMessageRequired)
|
|
),
|
|
});
|
|
|
|
return (
|
|
<Formik
|
|
initialValues={{
|
|
selectedIssue: issueOptions[0],
|
|
message: '',
|
|
problemSeason: 0,
|
|
problemEpisode: 0,
|
|
}}
|
|
validationSchema={CreateIssueModalSchema}
|
|
onSubmit={async (values) => {
|
|
try {
|
|
const newIssue = await axios.post<Issue>('/api/v1/issue', {
|
|
issueType: values.selectedIssue.issueType,
|
|
message: values.message,
|
|
mediaId: data?.mediaInfo?.id,
|
|
problemSeason: values.problemSeason,
|
|
problemEpisode:
|
|
values.problemSeason > 0 ? values.problemEpisode : 0,
|
|
});
|
|
|
|
if (data) {
|
|
addToast(
|
|
<>
|
|
<div>
|
|
{intl.formatMessage(messages.toastSuccessCreate, {
|
|
title: isMovie(data) ? data.title : data.name,
|
|
strong: function strong(msg) {
|
|
return <strong>{msg}</strong>;
|
|
},
|
|
})}
|
|
</div>
|
|
<Link href={`/issues/${newIssue.data.id}`}>
|
|
<Button as="a" className="mt-4">
|
|
<span>{intl.formatMessage(messages.toastviewissue)}</span>
|
|
<ArrowCircleRightIcon />
|
|
</Button>
|
|
</Link>
|
|
</>,
|
|
{
|
|
appearance: 'success',
|
|
autoDismiss: true,
|
|
}
|
|
);
|
|
}
|
|
|
|
if (onCancel) {
|
|
onCancel();
|
|
}
|
|
} catch (e) {
|
|
addToast(intl.formatMessage(messages.toastFailedCreate), {
|
|
appearance: 'error',
|
|
autoDismiss: true,
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
{({ handleSubmit, values, setFieldValue, errors, touched }) => {
|
|
return (
|
|
<Modal
|
|
backgroundClickable
|
|
onCancel={onCancel}
|
|
iconSvg={<ExclamationIcon />}
|
|
title={intl.formatMessage(messages.reportissue)}
|
|
cancelText={intl.formatMessage(globalMessages.close)}
|
|
onOk={() => handleSubmit()}
|
|
okText={intl.formatMessage(messages.submitissue)}
|
|
loading={!data && !error}
|
|
backdrop={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data?.backdropPath}`}
|
|
>
|
|
{data && (
|
|
<div className="flex items-center">
|
|
<span className="mr-1 font-semibold">
|
|
{intl.formatMessage(messages.issomethingwrong, {
|
|
title: isMovie(data) ? data.title : data.name,
|
|
})}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{mediaType === 'tv' && data && !isMovie(data) && (
|
|
<>
|
|
<div className="form-row">
|
|
<label htmlFor="problemSeason" className="text-label">
|
|
{intl.formatMessage(messages.problemseason)}
|
|
<span className="label-required">*</span>
|
|
</label>
|
|
<div className="form-input">
|
|
<div className="form-input-field">
|
|
<Field
|
|
as="select"
|
|
id="problemSeason"
|
|
name="problemSeason"
|
|
>
|
|
<option value={0}>
|
|
{intl.formatMessage(messages.allseasons)}
|
|
</option>
|
|
{data.seasons.map((season) => (
|
|
<option
|
|
value={season.seasonNumber}
|
|
key={`problem-season-${season.seasonNumber}`}
|
|
>
|
|
{intl.formatMessage(messages.season, {
|
|
seasonNumber: season.seasonNumber,
|
|
})}
|
|
</option>
|
|
))}
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{values.problemSeason > 0 && (
|
|
<div className="mb-2 form-row">
|
|
<label htmlFor="problemEpisode" className="text-label">
|
|
{intl.formatMessage(messages.problemepisode)}
|
|
<span className="label-required">*</span>
|
|
</label>
|
|
<div className="form-input">
|
|
<div className="form-input-field">
|
|
<Field
|
|
as="select"
|
|
id="problemEpisode"
|
|
name="problemEpisode"
|
|
>
|
|
<option value={0}>
|
|
{intl.formatMessage(messages.allepisodes)}
|
|
</option>
|
|
{[
|
|
...Array(
|
|
data.seasons.find(
|
|
(season) =>
|
|
Number(values.problemSeason) ===
|
|
season.seasonNumber
|
|
)?.episodeCount ?? 0
|
|
),
|
|
].map((i, index) => (
|
|
<option
|
|
value={index + 1}
|
|
key={`problem-episode-${index + 1}`}
|
|
>
|
|
{intl.formatMessage(messages.episode, {
|
|
episodeNumber: index + 1,
|
|
})}
|
|
</option>
|
|
))}
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
<RadioGroup
|
|
value={values.selectedIssue}
|
|
onChange={(issue) => setFieldValue('selectedIssue', issue)}
|
|
className="mt-4"
|
|
>
|
|
<RadioGroup.Label className="sr-only">
|
|
Select an Issue
|
|
</RadioGroup.Label>
|
|
<div className="-space-y-px overflow-hidden bg-gray-800 rounded-md bg-opacity-30">
|
|
{issueOptions.map((setting, index) => (
|
|
<RadioGroup.Option
|
|
key={`issue-type-${setting.issueType}`}
|
|
value={setting}
|
|
className={({ checked }) =>
|
|
classNames(
|
|
index === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
|
index === issueOptions.length - 1
|
|
? 'rounded-bl-md rounded-br-md'
|
|
: '',
|
|
checked
|
|
? 'bg-indigo-600 border-indigo-500 z-10'
|
|
: 'border-gray-500',
|
|
'relative border p-4 flex cursor-pointer focus:outline-none'
|
|
)
|
|
}
|
|
>
|
|
{({ active, checked }) => (
|
|
<>
|
|
<span
|
|
className={`${
|
|
checked
|
|
? 'bg-indigo-800 border-transparent'
|
|
: 'bg-white border-gray-300'
|
|
} ${
|
|
active ? 'ring-2 ring-offset-2 ring-indigo-300' : ''
|
|
} h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center`}
|
|
aria-hidden="true"
|
|
>
|
|
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
|
</span>
|
|
<div className="flex flex-col ml-3">
|
|
<RadioGroup.Label
|
|
as="span"
|
|
className={`block text-sm font-medium ${
|
|
checked ? 'text-indigo-100' : 'text-gray-100'
|
|
}`}
|
|
>
|
|
{intl.formatMessage(setting.name)}
|
|
</RadioGroup.Label>
|
|
</div>
|
|
</>
|
|
)}
|
|
</RadioGroup.Option>
|
|
))}
|
|
</div>
|
|
</RadioGroup>
|
|
<div className="flex-col mt-4 space-y-2">
|
|
<span>
|
|
{intl.formatMessage(messages.whatswrong)}{' '}
|
|
<span className="label-required">*</span>
|
|
</span>
|
|
<Field
|
|
as="textarea"
|
|
name="message"
|
|
id="message"
|
|
className="h-28"
|
|
placeholder={intl.formatMessage(messages.providedetail)}
|
|
/>
|
|
{errors.message && touched.message && (
|
|
<div className="error">{errors.message}</div>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}}
|
|
</Formik>
|
|
);
|
|
};
|
|
|
|
export default CreateIssueModal;
|