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,45 @@
import {useForm} from 'react-hook-form';
import {
CreatePersonPayload,
useCreatePerson,
} from '@app/admin/people/requests/use-create-person';
import {CrupdateResourceLayout} from '@common/admin/crupdate-resource-layout';
import {Trans} from '@common/i18n/trans';
import React from 'react';
import {FileUploadProvider} from '@common/uploads/uploader/file-upload-provider';
import {useNavigate} from '@common/utils/hooks/use-navigate';
import {PersonPrimaryFactsForm} from '@app/admin/people/crupdate/person-primary-facts-form';
export function CreatePersonPage() {
const navigate = useNavigate();
const form = useForm<CreatePersonPayload>({
defaultValues: {
gender: 'female',
known_for: 'Acting',
popularity: 3,
},
});
const createPerson = useCreatePerson(form);
return (
<CrupdateResourceLayout
onSubmit={values =>
createPerson.mutate(values, {
onSuccess: response => {
navigate(`../${response.person.id}/edit`, {
relative: 'path',
replace: true,
});
},
})
}
form={form}
title={<Trans message="New person" />}
isLoading={createPerson.isPending}
disableSaveWhenNotDirty
>
<FileUploadProvider>
<PersonPrimaryFactsForm />
</FileUploadProvider>
</CrupdateResourceLayout>
);
}

View File

@@ -0,0 +1,156 @@
import {ColumnConfig} from '@common/datatable/column-config';
import {PersonCredit, TitleCredit} from '@app/titles/models/title';
import {Trans} from '@common/i18n/trans';
import React, {Fragment, useContext, useMemo} from 'react';
import {Table, TableBodyProps} from '@common/ui/tables/table';
import {TableRow} from '@common/ui/tables/table-row';
import {TableContext} from '@common/ui/tables/table-context';
import {TitlePoster} from '@app/titles/title-poster/title-poster';
import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';
import {IconButton} from '@common/ui/buttons/icon-button';
import {DeleteIcon} from '@common/icons/material/Delete';
import {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';
import {useOutletContext} from 'react-router-dom';
import {GetPersonResponse} from '@app/people/requests/use-person';
import {IllustratedMessage} from '@common/ui/images/illustrated-message';
import {RecentActorsIcon} from '@common/icons/material/RecentActors';
import {TitleLink} from '@app/titles/title-link';
import {useDeletePersonCredit} from '@app/admin/people/requests/use-delete-person-credit';
const columnConfig: ColumnConfig<PersonCredit>[] = [
{
key: 'name',
header: () => <Trans message="Credit" />,
visibleInMode: 'all',
width: 'flex-3',
body: credit => (
<div className="flex items-center gap-12">
<TitlePoster title={credit} srcSize="sm" size="w-32" />
<div className="overflow-hidden min-w-0">
<div className="overflow-hidden overflow-ellipsis">
<TitleLink title={credit} target="_blank" />
</div>
<div className="text-muted text-xs overflow-hidden overflow-ellipsis">
{credit.is_series ? (
<Trans message="Series" />
) : (
<Trans message="Movie" />
)}
</div>
</div>
</div>
),
},
{
key: 'year',
header: () => <Trans message="Year" />,
body: credit => credit.year,
},
{
key: 'character',
header: () => <Trans message="Character" />,
body: credit => (credit.pivot.character ? credit.pivot.character : '-'),
},
{
key: 'department',
header: () => <Trans message="Department" />,
body: credit => (
<span className="capitalize">{credit.pivot.department}</span>
),
},
{
key: 'job',
header: () => <Trans message="Job" />,
body: credit => <span className="capitalize">{credit.pivot.job}</span>,
},
{
key: 'actions',
header: () => <Trans message="Actions" />,
hideHeader: true,
align: 'end',
width: 'w-42 flex-shrink-0',
visibleInMode: 'all',
body: item => (
<div className="text-muted">
<DeleteButton credit={item} />
</div>
),
},
];
export function PersonCreditsEditor() {
const data = useOutletContext<GetPersonResponse>();
const credits = useMemo(() => {
return Object.values(data.credits)
.flat()
.filter(credit => credit.pivot != null);
}, [data.credits]);
return (
<Fragment>
<Table
enableSelection={false}
columns={columnConfig}
data={credits}
cellHeight="h-54"
tableBody={<CreditsTableBody />}
/>
{!credits.length && <NoCreditsMessage />}
</Fragment>
);
}
function CreditsTableBody({renderRowAs}: TableBodyProps) {
const {data} = useContext(TableContext);
return (
<Fragment>
{data.map((item, rowIndex) => (
<TableRow
item={item}
index={rowIndex}
// use pivot id for key because some person might
// appear multiple times with different department
key={(item as TitleCredit).pivot.id}
renderAs={renderRowAs}
/>
))}
</Fragment>
);
}
function NoCreditsMessage() {
return (
<IllustratedMessage
className="mt-40"
imageMargin="mb-8"
image={
<div className="text-muted">
<RecentActorsIcon size="xl" />
</div>
}
imageHeight="h-auto"
title={<Trans message="No credits have been added yet" />}
/>
);
}
interface DeleteButtonProps {
credit: PersonCredit;
}
function DeleteButton({credit}: DeleteButtonProps) {
const deleteCredit = useDeletePersonCredit(credit);
return (
<DialogTrigger type="modal">
<IconButton>
<DeleteIcon />
</IconButton>
<ConfirmationDialog
isDanger
title={<Trans message="Delete credit" />}
body={<Trans message="Are you sure you want to delete this credit?" />}
confirm={<Trans message="Delete" />}
isLoading={deleteCredit.isPending}
onConfirm={() => deleteCredit.mutate()}
/>
</DialogTrigger>
);
}

View File

@@ -0,0 +1,113 @@
import React, {Fragment, useMemo} from 'react';
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 {FormSelect, Option} from '@common/ui/forms/select/select';
import {useValueLists} from '@common/http/value-lists';
import {Item} from '@common/ui/forms/listbox/item';
export function PersonPrimaryFactsForm() {
return (
<Fragment>
<div className="mb-24 gap-24 md:flex">
<FormImageSelector
variant="square"
previewSize="w-204 aspect-poster"
name="poster"
diskPrefix="person-posters"
label={<Trans message="Poster" />}
showRemoveButton
/>
<div className="flex-auto max-md:mt-24">
<FormTextField
name="name"
label={<Trans message="Name" />}
className="mb-24"
required
/>
<KnownForField />
<FormDatePicker
name="birth_date"
label={<Trans message="Birth date" />}
className="mb-24"
granularity="day"
/>
<FormDatePicker
name="death_date"
label={<Trans message="Death date" />}
granularity="day"
/>
</div>
</div>
<FormTextField
name="description"
label={<Trans message="Biography" />}
inputElementType="textarea"
rows={4}
className="mb-24"
/>
<div className="mb-24 items-center gap-24 md:flex">
<FormTextField
name="birth_place"
label={<Trans message="Birth place" />}
className="flex-1 max-md:mb-24"
/>
</div>
<div className="mb-24 items-center gap-24 md:flex">
<FormSelect
name="gender"
label={<Trans message="Gender" />}
className="flex-1 max-md:mb-24"
selectionMode="single"
>
<Option value="male">
<Trans message="Male" />
</Option>
<Option value="female">
<Trans message="Female" />
</Option>
</FormSelect>
</div>
<div className="mb-24 items-center gap-24 md:flex">
<FormTextField
name="popularity"
label={<Trans message="Popularity" />}
type="number"
min={1}
className="flex-1 max-md:mb-24"
/>
</div>
</Fragment>
);
}
function KnownForField() {
const {data} = useValueLists(['tmdbDepartments']);
const departments = useMemo(() => {
return data?.tmdbDepartments.map(item => {
if (item.department === 'Actors') {
return {department: 'Acting'};
}
return {department: item.department};
});
}, [data]);
return (
<FormSelect
name="known_for"
label={<Trans message="Known for" />}
required
items={departments}
className="mb-24"
selectionMode="single"
showSearchField
>
{item => (
<Item value={item.department}>
<Trans message={item.department} />
</Item>
)}
</FormSelect>
);
}

View File

@@ -0,0 +1,93 @@
import {useForm} from 'react-hook-form';
import {CreatePersonPayload} from '@app/admin/people/requests/use-create-person';
import {CrupdateResourceLayout} from '@common/admin/crupdate-resource-layout';
import {Trans} from '@common/i18n/trans';
import React, {Fragment} from 'react';
import {FileUploadProvider} from '@common/uploads/uploader/file-upload-provider';
import {useNavigate} from '@common/utils/hooks/use-navigate';
import {useUpdatePerson} from '@app/admin/people/requests/use-update-person';
import {GetPersonResponse, usePerson} from '@app/people/requests/use-person';
import {PageMetaTags} from '@common/http/page-meta-tags';
import {PageStatus} from '@common/http/page-status';
import {Tabs} from '@common/ui/tabs/tabs';
import {TabList} from '@common/ui/tabs/tab-list';
import {Tab} from '@common/ui/tabs/tab';
import {Link, Outlet, useLocation} from 'react-router-dom';
export function UpdatePersonPage() {
const query = usePerson('editPersonPage');
return query.data ? (
<Fragment>
<PageMetaTags query={query} />
<PageContent data={query.data} />
</Fragment>
) : (
<div className="relative h-full w-full">
<PageStatus query={query} loaderClassName="absolute inset-0 m-auto" />
</div>
);
}
interface PageContentProps {
data: GetPersonResponse;
}
function PageContent({data}: PageContentProps) {
const {person} = data;
const navigate = useNavigate();
const form = useForm<CreatePersonPayload>({
defaultValues: {
name: person.name,
known_for: person.known_for,
poster: person.poster,
birth_date: person.birth_date,
death_date: person.death_date,
birth_place: person.birth_place,
description: person.description,
gender: person.gender,
popularity: person.popularity,
},
});
const updatePersonPage = useUpdatePerson(form);
const {pathname} = useLocation();
const tabName = pathname.split('/').pop();
const selectedTab = tabName === 'credits' ? 1 : 0;
return (
<CrupdateResourceLayout
onSubmit={values =>
updatePersonPage.mutate(values, {
onSuccess: () => {
navigate('../../../', {relative: 'path', replace: true});
},
})
}
form={form}
title={<Trans values={{name: person.name}} message="Edit “:name“" />}
isLoading={updatePersonPage.isPending}
disableSaveWhenNotDirty
>
<Tabs selectedTab={selectedTab}>
<TabList>
<Tab
elementType={Link}
to={`../primary-facts`}
relative="path"
replace
>
<Trans message="Primary facts" />
</Tab>
<Tab elementType={Link} to={`../credits`} relative="path" replace>
<Trans message="Credits" />
</Tab>
</TabList>
<div className="min-h-512 pt-24">
<FileUploadProvider>
<Outlet context={data} />
</FileUploadProvider>
</div>
</Tabs>
</CrupdateResourceLayout>
);
}