first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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()});
}}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}