@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import {EpisodeEditorLayout} from '@app/admin/titles/title-editor/episode-editor/episode-editor-layout';
|
||||
import {CastEditorTable} from '@app/admin/titles/title-editor/credits-editor/cast-editor-table';
|
||||
import {useTitleCredits} from '@app/admin/titles/requests/use-title-credits';
|
||||
import {TitleCreditsTableHeader} from '@app/admin/titles/title-editor/credits-editor/title-credits-table-header';
|
||||
|
||||
export function EpisodeCastEditor() {
|
||||
const query = useTitleCredits({
|
||||
department: 'actors',
|
||||
});
|
||||
|
||||
return (
|
||||
<EpisodeEditorLayout>
|
||||
<TitleCreditsTableHeader query={query} isCrew={false} />
|
||||
<CastEditorTable query={query} />
|
||||
</EpisodeEditorLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import {EpisodeEditorLayout} from '@app/admin/titles/title-editor/episode-editor/episode-editor-layout';
|
||||
import {useTitleCredits} from '@app/admin/titles/requests/use-title-credits';
|
||||
import {TitleCreditsTableHeader} from '@app/admin/titles/title-editor/credits-editor/title-credits-table-header';
|
||||
import {CrewEditorTable} from '@app/admin/titles/title-editor/credits-editor/crew-editor-table';
|
||||
|
||||
export function EpisodeCrewEditor() {
|
||||
const query = useTitleCredits({
|
||||
crewOnly: 'true',
|
||||
});
|
||||
|
||||
return (
|
||||
<EpisodeEditorLayout>
|
||||
<TitleCreditsTableHeader query={query} isCrew />
|
||||
<CrewEditorTable query={query} />
|
||||
</EpisodeEditorLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {FileUploadProvider} from '@common/uploads/uploader/file-upload-provider';
|
||||
import React, {ReactNode} from 'react';
|
||||
import {Breadcrumb} from '@common/ui/breadcrumbs/breadcrumb';
|
||||
import {BreadcrumbItem} from '@common/ui/breadcrumbs/breadcrumb-item';
|
||||
import {useNavigate} from '@common/utils/hooks/use-navigate';
|
||||
import {Tabs} from '@common/ui/tabs/tabs';
|
||||
import {TabList} from '@common/ui/tabs/tab-list';
|
||||
import {Tab} from '@common/ui/tabs/tab';
|
||||
import {TitleEditorLayout} from '@app/admin/titles/title-editor/title-editor-layout';
|
||||
import {Link, useLocation, useParams} from 'react-router-dom';
|
||||
import {message} from '@common/i18n/message';
|
||||
|
||||
const PageTabs = [
|
||||
{uri: 'primary-facts', label: message('Primary facts')},
|
||||
{uri: 'cast', label: message('Cast')},
|
||||
{uri: 'crew', label: message('Crew')},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
export function EpisodeEditorLayout({children, actions}: Props) {
|
||||
const {episode, season} = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {pathname} = useLocation();
|
||||
const tabName = pathname.split('/').pop();
|
||||
|
||||
// only "primary facts" tab will be enabled when creating new episode
|
||||
const selectedTab = episode
|
||||
? PageTabs.findIndex(tab => tab.uri === tabName)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<TitleEditorLayout actions={actions}>
|
||||
<Breadcrumb className="mb-24">
|
||||
<BreadcrumbItem
|
||||
onSelected={() => navigate('../..', {relative: 'path'})}
|
||||
>
|
||||
<Trans message="Season :number" values={{number: season}} />
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem>
|
||||
{episode ? (
|
||||
<Trans message="Episode :number" values={{number: episode}} />
|
||||
) : (
|
||||
<Trans message="New episode" />
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
<FileUploadProvider>
|
||||
<Tabs selectedTab={selectedTab}>
|
||||
<TabList>
|
||||
{PageTabs.map(tab => (
|
||||
<Tab
|
||||
isDisabled={!episode && tab.uri !== 'primary-facts'}
|
||||
key={tab.uri}
|
||||
width="min-w-132"
|
||||
elementType={Link}
|
||||
to={`../${tab.uri}`}
|
||||
relative="path"
|
||||
replace
|
||||
>
|
||||
<Trans {...tab.label} />
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<div className="pt-24">{children}</div>
|
||||
</Tabs>
|
||||
</FileUploadProvider>
|
||||
</TitleEditorLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import {FormImageSelector} from '@common/ui/images/image-selector';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
|
||||
import {FormDatePicker} from '@common/ui/forms/input-field/date/date-picker/date-picker';
|
||||
import React, {Fragment} from 'react';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {useOutletContext, useParams} from 'react-router-dom';
|
||||
import {Form} from '@common/ui/forms/form';
|
||||
import {useUpdateEpisode} from '@app/episodes/requests/use-update-episode';
|
||||
import {EpisodeEditorLayout} from '@app/admin/titles/title-editor/episode-editor/episode-editor-layout';
|
||||
import {toast} from '@common/ui/toast/toast';
|
||||
import {message} from '@common/i18n/message';
|
||||
import {
|
||||
CreateEpisodePayload,
|
||||
useCreateEpisode,
|
||||
} from '@app/episodes/requests/use-create-episode';
|
||||
import {useNavigate} from '@common/utils/hooks/use-navigate';
|
||||
import {Button} from '@common/ui/buttons/button';
|
||||
import {useCurrentDateTime} from '@common/i18n/use-current-date-time';
|
||||
import {Title} from '@app/titles/models/title';
|
||||
import {
|
||||
GetEpisodeResponse,
|
||||
useEpisode,
|
||||
} from '@app/episodes/requests/use-episode';
|
||||
import {TitleEditorPageStatus} from '@app/admin/titles/title-editor/title-editor-page-status';
|
||||
|
||||
export function EpisodePrimaryFactsForm() {
|
||||
const {episode: episodeNumber} = useParams();
|
||||
if (episodeNumber) {
|
||||
return <UpdateEpisodePanel />;
|
||||
} else {
|
||||
return <NewEpisodeForm />;
|
||||
}
|
||||
}
|
||||
|
||||
function NewEpisodeForm() {
|
||||
const title = useOutletContext<Title>();
|
||||
const {season} = useParams();
|
||||
const navigate = useNavigate();
|
||||
const now = useCurrentDateTime();
|
||||
const form = useForm<CreateEpisodePayload>({
|
||||
defaultValues: {
|
||||
release_date: now.toAbsoluteString(),
|
||||
},
|
||||
});
|
||||
const createEpisode = useCreateEpisode(form, title.id, season!);
|
||||
const isDirty = Object.keys(form.formState.dirtyFields).length > 0;
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onSubmit={values => {
|
||||
createEpisode.mutate(values, {
|
||||
onSuccess: response => {
|
||||
toast(message('Episode created'));
|
||||
navigate(`../${response.episode.episode_number}`, {
|
||||
relative: 'path',
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EpisodeEditorLayout
|
||||
actions={
|
||||
<Button
|
||||
variant="flat"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={createEpisode.isPending || !isDirty}
|
||||
>
|
||||
<Trans message="Save" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<FormFields />
|
||||
</EpisodeEditorLayout>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function UpdateEpisodePanel() {
|
||||
const query = useEpisode('episode');
|
||||
return query.data ? (
|
||||
<UpdateEpisodeForm episode={query.data.episode} />
|
||||
) : (
|
||||
<EpisodeEditorLayout
|
||||
actions={
|
||||
<Button variant="flat" color="primary" type="submit" disabled>
|
||||
<Trans message="Save" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<TitleEditorPageStatus query={query} />
|
||||
</EpisodeEditorLayout>
|
||||
);
|
||||
}
|
||||
|
||||
interface UpdateEpisodeFormProps {
|
||||
episode: GetEpisodeResponse['episode'];
|
||||
}
|
||||
function UpdateEpisodeForm({episode}: UpdateEpisodeFormProps) {
|
||||
const title = useOutletContext<Title>();
|
||||
const navigate = useNavigate();
|
||||
const form = useForm<CreateEpisodePayload>({
|
||||
defaultValues: {
|
||||
name: episode.name,
|
||||
description: episode.description,
|
||||
release_date: episode.release_date,
|
||||
runtime: episode.runtime,
|
||||
popularity: episode.popularity,
|
||||
poster: episode.poster,
|
||||
},
|
||||
});
|
||||
|
||||
const updateEpisode = useUpdateEpisode(
|
||||
title.id,
|
||||
episode.season_number,
|
||||
episode.episode_number,
|
||||
form,
|
||||
);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onSubmit={values => {
|
||||
updateEpisode.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast(message('Episode updated'));
|
||||
navigate('../../../', {relative: 'path'});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EpisodeEditorLayout
|
||||
actions={
|
||||
<Button
|
||||
variant="flat"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={updateEpisode.isPending || !form.formState.isDirty}
|
||||
>
|
||||
<Trans message="Save" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<FormFields />
|
||||
</EpisodeEditorLayout>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function FormFields() {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="gap-24 md:flex">
|
||||
<FormImageSelector
|
||||
variant="square"
|
||||
previewSize="w-204 aspect-poster"
|
||||
name="poster"
|
||||
diskPrefix="episode-posters"
|
||||
label={<Trans message="Poster" />}
|
||||
stretchPreview
|
||||
/>
|
||||
<div className="mb-24 flex-auto max-md:mt-24">
|
||||
<FormTextField
|
||||
name="name"
|
||||
label={<Trans message="Title" />}
|
||||
className="mb-24"
|
||||
required
|
||||
/>
|
||||
<FormDatePicker
|
||||
name="release_date"
|
||||
label={<Trans message="Release date" />}
|
||||
className="mb-24"
|
||||
granularity="day"
|
||||
/>
|
||||
<FormTextField
|
||||
name="runtime"
|
||||
label={<Trans message="Runtime" />}
|
||||
type="number"
|
||||
min={1}
|
||||
className="mb-24"
|
||||
/>
|
||||
<FormTextField
|
||||
name="popularity"
|
||||
label={<Trans message="Popularity" />}
|
||||
type="number"
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormTextField
|
||||
name="description"
|
||||
label={<Trans message="Overview" />}
|
||||
inputElementType="textarea"
|
||||
rows={6}
|
||||
className="mb-24"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user