45
resources/client/admin/people/crupdate/create-person-page.tsx
Executable file
45
resources/client/admin/people/crupdate/create-person-page.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
156
resources/client/admin/people/crupdate/person-credits-editor.tsx
Executable file
156
resources/client/admin/people/crupdate/person-credits-editor.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
113
resources/client/admin/people/crupdate/person-primary-facts-form.tsx
Executable file
113
resources/client/admin/people/crupdate/person-primary-facts-form.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
93
resources/client/admin/people/crupdate/update-person-page.tsx
Executable file
93
resources/client/admin/people/crupdate/update-person-page.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user