@@ -0,0 +1,16 @@
|
||||
import {CastEditorTable} from '@app/admin/titles/title-editor/credits-editor/cast-editor-table';
|
||||
import {useTitleCredits} from '@app/admin/titles/requests/use-title-credits';
|
||||
import {SeasonEditorLayout} from '@app/admin/titles/title-editor/seasons-editor/season-editor-layout';
|
||||
import {TitleCreditsTableHeader} from '@app/admin/titles/title-editor/credits-editor/title-credits-table-header';
|
||||
|
||||
export function SeasonCastEditor() {
|
||||
const query = useTitleCredits({
|
||||
department: 'actors',
|
||||
});
|
||||
return (
|
||||
<SeasonEditorLayout>
|
||||
<TitleCreditsTableHeader query={query} isCrew={false} />
|
||||
<CastEditorTable query={query} />
|
||||
</SeasonEditorLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import {useTitleCredits} from '@app/admin/titles/requests/use-title-credits';
|
||||
import {SeasonEditorLayout} from '@app/admin/titles/title-editor/seasons-editor/season-editor-layout';
|
||||
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 SeasonCrewEditor() {
|
||||
const query = useTitleCredits({
|
||||
crewOnly: 'true',
|
||||
});
|
||||
return (
|
||||
<SeasonEditorLayout>
|
||||
<TitleCreditsTableHeader query={query} isCrew />
|
||||
<CrewEditorTable query={query} />
|
||||
</SeasonEditorLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import {GetSeasonResponse, useSeason} from '@app/seasons/requests/use-season';
|
||||
import React, {Fragment} from 'react';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {IconButton} from '@common/ui/buttons/icon-button';
|
||||
import {Button} from '@common/ui/buttons/button';
|
||||
import {EditIcon} from '@common/icons/material/Edit';
|
||||
import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';
|
||||
import {DeleteIcon} from '@common/icons/material/Delete';
|
||||
import {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';
|
||||
import {useDeleteEpisode} from '@app/episodes/requests/use-delete-episode';
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
import {EpisodeListItem} from '@app/seasons/episode-list-item';
|
||||
import {AddIcon} from '@common/icons/material/Add';
|
||||
import {SeasonEditorLayout} from '@app/admin/titles/title-editor/seasons-editor/season-editor-layout';
|
||||
import {useSeasonEpisodes} from '@app/titles/requests/use-season-episodes';
|
||||
import {InfiniteScrollSentinel} from '@common/ui/infinite-scroll/infinite-scroll-sentinel';
|
||||
import {TitleEditorPageStatus} from '@app/admin/titles/title-editor/title-editor-page-status';
|
||||
import {IllustratedMessage} from '@common/ui/images/illustrated-message';
|
||||
import {TvIcon} from '@common/icons/material/Tv';
|
||||
import {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';
|
||||
|
||||
export function SeasonEditorEpisodeList() {
|
||||
return (
|
||||
<SeasonEditorLayout>
|
||||
<div className="mb-16">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
size="xs"
|
||||
elementType={Link}
|
||||
to="new"
|
||||
>
|
||||
<Trans message="Add episode" />
|
||||
</Button>
|
||||
</div>
|
||||
<Content />
|
||||
</SeasonEditorLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const query = useSeason('editSeasonPage');
|
||||
if (query.data) {
|
||||
return query.data.episodes?.data.length ? (
|
||||
<LazyEpisodeList data={query.data} />
|
||||
) : (
|
||||
<NoEpisodesMessage />
|
||||
);
|
||||
} else {
|
||||
return <TitleEditorPageStatus query={query} />;
|
||||
}
|
||||
}
|
||||
|
||||
function NoEpisodesMessage() {
|
||||
return (
|
||||
<IllustratedMessage
|
||||
className="mt-40"
|
||||
imageMargin="mb-8"
|
||||
image={
|
||||
<div className="text-muted">
|
||||
<TvIcon size="xl" />
|
||||
</div>
|
||||
}
|
||||
imageHeight="h-auto"
|
||||
title={<Trans message="No episodes have been added yet" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface LazyEpisodeListProps {
|
||||
data: GetSeasonResponse;
|
||||
}
|
||||
function LazyEpisodeList({data}: LazyEpisodeListProps) {
|
||||
const query = useSeasonEpisodes(data.episodes);
|
||||
return (
|
||||
<Fragment>
|
||||
{query.items.map(episode => (
|
||||
<EpisodeListItem
|
||||
key={episode.id}
|
||||
episode={episode}
|
||||
title={data.title}
|
||||
className="mb-24"
|
||||
>
|
||||
<div className="mt-12 flex items-center gap-12">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
startIcon={<EditIcon />}
|
||||
elementType={Link}
|
||||
to={`${episode.episode_number}/primary-facts`}
|
||||
>
|
||||
<Trans message="Edit" />
|
||||
</Button>
|
||||
<DialogTrigger type="modal">
|
||||
<IconButton size="xs" variant="outline">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<DeleteEpisodeDialog episode={episode} />
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
</EpisodeListItem>
|
||||
))}
|
||||
<InfiniteScrollSentinel query={query} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface DeleteEpisodeDialogProps {
|
||||
episode: Episode;
|
||||
}
|
||||
function DeleteEpisodeDialog({episode}: DeleteEpisodeDialogProps) {
|
||||
const deleteEpisode = useDeleteEpisode(episode);
|
||||
const {close} = useDialogContext();
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
isLoading={deleteEpisode.isPending}
|
||||
isDanger
|
||||
title={<Trans message="Delete episode" />}
|
||||
body={<Trans message="Are you sure you want to delete this episode?" />}
|
||||
confirm={<Trans message="Delete" />}
|
||||
onConfirm={() => {
|
||||
deleteEpisode.mutate(undefined, {onSuccess: () => close()});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, {ReactNode} from 'react';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {Link, useLocation, useParams} from 'react-router-dom';
|
||||
import {IconButton} from '@common/ui/buttons/icon-button';
|
||||
import {ArrowBackIcon} from '@common/icons/material/ArrowBack';
|
||||
import {TitleEditorLayout} from '@app/admin/titles/title-editor/title-editor-layout';
|
||||
import {TabList} from '@common/ui/tabs/tab-list';
|
||||
import {Tab} from '@common/ui/tabs/tab';
|
||||
import {Tabs} from '@common/ui/tabs/tabs';
|
||||
import {message} from '@common/i18n/message';
|
||||
|
||||
const PageTabs = [
|
||||
{uri: 'episodes', label: message('Episodes')},
|
||||
{uri: 'cast', label: message('Regular cast')},
|
||||
{uri: 'crew', label: message('Regular crew')},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
export function SeasonEditorLayout({children}: Props) {
|
||||
const {season: seasonNumber} = useParams();
|
||||
|
||||
const {pathname} = useLocation();
|
||||
const tabName = pathname.split('/').pop();
|
||||
|
||||
// only "episodes" tab will be enabled when creating new episode
|
||||
const selectedTab = seasonNumber
|
||||
? PageTabs.findIndex(tab => tab.uri === tabName)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<TitleEditorLayout>
|
||||
<div className="flex items-center gap-12 mb-4">
|
||||
<IconButton
|
||||
elementType={Link}
|
||||
to="../../"
|
||||
relative="path"
|
||||
className="text-muted"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<h2 className="text-base">
|
||||
<Trans message="Season :number" values={{number: seasonNumber}} />
|
||||
</h2>
|
||||
</div>
|
||||
<Tabs selectedTab={selectedTab}>
|
||||
<TabList>
|
||||
{PageTabs.map(tab => (
|
||||
<Tab
|
||||
isDisabled={!seasonNumber && tab.uri !== PageTabs[0].uri}
|
||||
key={tab.uri}
|
||||
width="min-w-132"
|
||||
elementType={Link}
|
||||
to={`../${tab.uri}`}
|
||||
relative="path"
|
||||
replace
|
||||
>
|
||||
<Trans {...tab.label} />
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<div className="pt-24 min-h-512">{children}</div>
|
||||
</Tabs>
|
||||
</TitleEditorLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import {Title} from '@app/titles/models/title';
|
||||
import {Season} from '@app/titles/models/season';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {Link, useOutletContext} from 'react-router-dom';
|
||||
import {IconButton} from '@common/ui/buttons/icon-button';
|
||||
import {DeleteIcon} from '@common/icons/material/Delete';
|
||||
import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';
|
||||
import {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';
|
||||
import React, {Fragment} from 'react';
|
||||
import {useDeleteSeason} from '@app/admin/titles/requests/use-delete-season';
|
||||
import {useTitleSeasons} from '@app/titles/requests/use-title-seasons';
|
||||
import {TitleEditorLayout} from '@app/admin/titles/title-editor/title-editor-layout';
|
||||
import {TitleEditorPageStatus} from '@app/admin/titles/title-editor/title-editor-page-status';
|
||||
import {EditIcon} from '@common/icons/material/Edit';
|
||||
import {SeasonPoster} from '@app/seasons/season-poster';
|
||||
import {SeasonLink} from '@app/seasons/season-link';
|
||||
import {FormattedDate} from '@common/i18n/formatted-date';
|
||||
import {Button} from '@common/ui/buttons/button';
|
||||
import {IllustratedMessage} from '@common/ui/images/illustrated-message';
|
||||
import {TvIcon} from '@common/icons/material/Tv';
|
||||
import {InfiniteScrollSentinel} from '@common/ui/infinite-scroll/infinite-scroll-sentinel';
|
||||
import {AddIcon} from '@common/icons/material/Add';
|
||||
import {useCreateSeason} from '@app/admin/titles/requests/use-create-season';
|
||||
|
||||
export function TitleSeasonsEditor() {
|
||||
const title = useOutletContext<Title>();
|
||||
const createSeason = useCreateSeason(title.id);
|
||||
const query = useTitleSeasons(title.id, undefined, {
|
||||
perPage: 15,
|
||||
});
|
||||
|
||||
let content;
|
||||
if (query.data) {
|
||||
content = query.items.length ? (
|
||||
<Fragment>
|
||||
<div className="mt-24 grid grid-cols-2 gap-24 md:grid-cols-5">
|
||||
{query.items.map(season => (
|
||||
<div key={season.id}>
|
||||
<SeasonPoster
|
||||
title={title}
|
||||
season={season}
|
||||
srcSize="md"
|
||||
className="aspect-poster flex-shrink-0"
|
||||
/>
|
||||
<div className="mt-8">
|
||||
<div className="flex items-center justify-between gap-14">
|
||||
<SeasonLink title={title} seasonNumber={season.number} />
|
||||
<div className="text-xs text-muted">
|
||||
<FormattedDate
|
||||
date={season.release_date}
|
||||
options={{year: 'numeric'}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-sm">
|
||||
{
|
||||
<Trans
|
||||
message=":count episodes"
|
||||
values={{count: season.episodes_count}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className="mt-14 flex items-center justify-between gap-14">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
startIcon={<EditIcon />}
|
||||
elementType={Link}
|
||||
to={`${season.number}/episodes`}
|
||||
>
|
||||
<Trans message="Edit" />
|
||||
</Button>
|
||||
<DeleteButton title={title} season={season} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<InfiniteScrollSentinel query={query} />
|
||||
</Fragment>
|
||||
) : (
|
||||
<NoSeasonsMessage />
|
||||
);
|
||||
} else {
|
||||
content = <TitleEditorPageStatus query={query} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TitleEditorLayout>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
disabled={createSeason.isPending}
|
||||
onClick={() => createSeason.mutate()}
|
||||
>
|
||||
<Trans message="Add season" />
|
||||
</Button>
|
||||
{content}
|
||||
</TitleEditorLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function NoSeasonsMessage() {
|
||||
return (
|
||||
<IllustratedMessage
|
||||
className="mt-40"
|
||||
imageMargin="mb-8"
|
||||
image={
|
||||
<div className="text-muted">
|
||||
<TvIcon size="xl" />
|
||||
</div>
|
||||
}
|
||||
imageHeight="h-auto"
|
||||
title={<Trans message="No seasons have been added yet" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface DeleteButtonProps {
|
||||
title: Title;
|
||||
season: Season;
|
||||
}
|
||||
function DeleteButton({title, season}: DeleteButtonProps) {
|
||||
const deleteSeason = useDeleteSeason(title, season.id);
|
||||
return (
|
||||
<DialogTrigger
|
||||
type="modal"
|
||||
onClose={confirmed => {
|
||||
if (confirmed) {
|
||||
deleteSeason.mutate();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconButton size="xs" variant="outline">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<ConfirmationDialog
|
||||
isDanger
|
||||
title={<Trans message="Delete season" />}
|
||||
body={<Trans message="Are you sure you want to delete this season?" />}
|
||||
confirm={<Trans message="Delete" />}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user