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

View File

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

View File

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

View File

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