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,24 @@
import {EpisodeCredit, PersonCredit} from '@app/titles/models/title';
import {Trans} from '@common/i18n/trans';
interface Props {
credit: PersonCredit | EpisodeCredit;
className?: string;
}
export function CharacterOrJob({credit, className}: Props) {
return (
<div className={className}>
{credit.pivot?.department === 'actors' ? (
credit.pivot?.character ?? <Trans message="Unknown" />
) : (
<span className="capitalize">
{credit.pivot?.job ? (
<Trans message={credit.pivot?.job} />
) : (
<Trans message="Unknown" />
)}
</span>
)}
</div>
);
}

View File

@@ -0,0 +1,111 @@
import {
DetailItem,
TitlePageAsideLayout,
} from '@app/titles/pages/title-page/title-page-aside-layout';
import {PersonPoster} from '@app/people/person-poster/person-poster';
import {Trans} from '@common/i18n/trans';
import {FormattedDate} from '@common/i18n/formatted-date';
import React from 'react';
import {PersonAge} from '@app/people/person-age';
import {GetPersonResponse} from '@app/people/requests/use-person';
import {ShareMenuTrigger} from '@app/sharing/share-menu-trigger';
import {Button} from '@common/ui/buttons/button';
import {ShareIcon} from '@common/icons/material/Share';
import {Person} from '@app/titles/models/person';
import {getPersonLink} from '@app/people/person-link';
import {IconButton} from '@common/ui/buttons/icon-button';
import {Link} from 'react-router-dom';
import {EditIcon} from '@common/icons/material/Edit';
import {useAuth} from '@common/auth/use-auth';
interface Props {
data: GetPersonResponse;
}
export function PersonPageAside({data: {person, total_credits_count}}: Props) {
const {hasPermission} = useAuth();
const age = (
<Trans
message=":count years old"
values={{count: <PersonAge person={person} />}}
/>
);
return (
<TitlePageAsideLayout
className="max-md:flex"
poster={
<div>
<div className="relative">
<PersonPoster person={person} size="w-140 md:w-full" srcSize="lg" />
{hasPermission('titles.update') && (
<IconButton
elementType={Link}
to={`/admin/people/${person.id}/edit`}
className="absolute bottom-6 right-4"
color="white"
>
<EditIcon />
</IconButton>
)}
</div>
<ShareButton person={person} />
</div>
}
>
<dl className="mt-12 md:mt-24">
{person.known_for && (
<DetailItem label={<Trans message="Known for" />}>
<Trans message={person.known_for} />
</DetailItem>
)}
{person.gender && (
<DetailItem label={<Trans message="Gender" />}>
<span className="capitalize">
<Trans message={person.gender} />
</span>
</DetailItem>
)}
{total_credits_count ? (
<DetailItem label={<Trans message="Known credits" />}>
{total_credits_count}
</DetailItem>
) : null}
{person.birth_date ? (
<DetailItem label={<Trans message="Born" />}>
<FormattedDate date={person.birth_date} />{' '}
{!person.death_date && age}
</DetailItem>
) : null}
{person.birth_place ? (
<DetailItem label={<Trans message="Birthplace" />}>
{person.birth_place}
</DetailItem>
) : null}
{person.death_date ? (
<DetailItem label={<Trans message="Died" />}>
<FormattedDate date={person.death_date} /> ({age})
</DetailItem>
) : null}
</dl>
</TitlePageAsideLayout>
);
}
interface ShareButtonProps {
person: Person;
}
function ShareButton({person}: ShareButtonProps) {
const link = getPersonLink(person, {absolute: true});
return (
<ShareMenuTrigger link={link}>
<Button
variant="outline"
color="primary"
startIcon={<ShareIcon />}
className="mt-14 md:min-h-40 md:w-full"
>
<Trans message="Share" />
</Button>
</ShareMenuTrigger>
);
}

View File

@@ -0,0 +1,151 @@
import {GetPersonResponse} from '@app/people/requests/use-person';
import {Accordion, AccordionItem} from '@common/ui/accordion/accordion';
import {Trans} from '@common/i18n/trans';
import {TitlePoster} from '@app/titles/title-poster/title-poster';
import {SiteSectionHeading} from '@app/titles/site-section-heading';
import {TitleLink} from '@app/titles/title-link';
import {PersonCredit} from '@app/titles/models/title';
import {BulletSeparatedItems} from '@app/titles/bullet-separated-items';
import {CompactSeasonEpisode} from '@app/episodes/compact-season-episode';
import {EpisodeLink} from '@app/episodes/episode-link';
import {Button} from '@common/ui/buttons/button';
import {Fragment, useState} from 'react';
import {useFullPersonCreditsForTitle} from '@app/people/requests/use-full-person-credits-for-title';
import {CharacterOrJob} from '@app/people/person-page/character-or-job';
import {Person} from '@app/titles/models/person';
interface Props {
data: GetPersonResponse;
}
export function PersonPageCredits({data: {credits, person}}: Props) {
return (
<div className="mt-34">
<SiteSectionHeading fontSize="text-xl">
<Trans message="Credits" />
</SiteSectionHeading>
<Accordion mode="multiple" defaultExpandedValues={[0]} isLazy>
{Object.entries(credits).map(([department, credits]) => (
<AccordionItem
labelClassName="font-semibold text-base"
description={
<Trans
message="(:count credits)"
values={{count: credits.length}}
/>
}
key={department}
label={
<span className="capitalize">
<Trans
message={department === 'actors' ? 'Acting' : department}
/>
</span>
}
>
{credits.map((credit, index) => {
const isLast = credit === credits[credits.length - 1];
return (
<Fragment key={credit.id}>
<div className="flex items-start py-6">
<TitlePoster
title={credit}
size="w-40"
className="mr-12"
lazy
srcSize="sm"
/>
<div className="mr-24 pt-2">
<div className="font-semibold text-base">
<TitleLink title={credit} />
</div>
<CharacterOrJob
className="text-sm text-muted"
credit={credit}
/>
{credit.credited_episode_count ? (
<EpisodeList
credit={credit}
department={department}
person={person}
/>
) : null}
</div>
<div className="text-sm text-muted ml-auto">
{credit.year}
</div>
</div>
{!isLast && credit.year !== credits[index + 1]?.year && (
<div className="h-1 w-full bg-divider my-8" />
)}
</Fragment>
);
})}
</AccordionItem>
))}
</Accordion>
</div>
);
}
interface EpisodeListProps {
credit: PersonCredit;
department: string;
person: Person;
}
function EpisodeList({credit, department, person}: EpisodeListProps) {
const [loadMoreEpisodes, setLoadMoreEpisodes] = useState(false);
const query = useFullPersonCreditsForTitle(
{person, department, credit},
{
enabled: loadMoreEpisodes,
}
);
const allEpisodesLoaded =
credit.episodes.length === credit.credited_episode_count ||
query.data != null;
const isLoadingMore = query.isLoading && query.fetchStatus !== 'idle';
const shouldShowLoadMoreBtn = isLoadingMore || !allEpisodesLoaded;
const episodeCredits = query.data?.credits.length
? query.data.credits
: credit.episodes;
return (
<div className="mt-4">
<div>
{episodeCredits.map(episodeCredit => (
<div className="text-xs pl-10 mb-4" key={episodeCredit.id}>
<BulletSeparatedItems>
<span>
-{' '}
<EpisodeLink
title={credit}
episode={episodeCredit}
seasonNumber={episodeCredit.season_number}
/>{' '}
({episodeCredit.year})
</span>
<CompactSeasonEpisode episode={episodeCredit} />
<CharacterOrJob credit={episodeCredit} />
</BulletSeparatedItems>
</div>
))}
</div>
{shouldShowLoadMoreBtn && (
<div className="mt-8">
<Button
size="xs"
disabled={isLoadingMore}
onClick={() => {
setLoadMoreEpisodes(true);
}}
>
<Trans
message="Show all :count episodes"
values={{count: credit.credited_episode_count}}
/>
</Button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,98 @@
import {PageStatus} from '@common/http/page-status';
import {PageMetaTags} from '@common/http/page-meta-tags';
import React, {Fragment} from 'react';
import {SitePageLayout} from '@app/site-page-layout';
import {GetPersonResponse, usePerson} from '@app/people/requests/use-person';
import {Person} from '@app/titles/models/person';
import {Trans} from '@common/i18n/trans';
import {TitlePageHeaderLayout} from '@app/titles/pages/title-page/title-page-header-layout';
import {SiteSectionHeading} from '@app/titles/site-section-heading';
import {PersonPageAside} from '@app/people/person-page/person-page-aside';
import {ContentGridLayout} from '@app/channels/content-grid/content-grid-layout';
import {PersonCredit} from '@app/titles/models/title';
import {TitlePortraitGridItem} from '@app/channels/content-grid/title-grid-item';
import {PersonPageCredits} from '@app/people/person-page/person-page-credits';
import {TruncatedDescription} from '@common/ui/truncated-description';
import {CharacterOrJob} from '@app/people/person-page/character-or-job';
import {AdHost} from '@common/admin/ads/ad-host';
export function PersonPage() {
const query = usePerson('personPage');
const content = query.data ? (
<Fragment>
<PageMetaTags query={query} />
<PageContent data={query.data} />
</Fragment>
) : (
<PageStatus query={query} loaderClassName="absolute inset-0 m-auto" />
);
return <SitePageLayout>{content}</SitePageLayout>;
}
interface PageContentProps {
data: GetPersonResponse;
}
function PageContent({data}: PageContentProps) {
const {person, knownFor} = data;
return (
<Fragment>
<div className="container mx-auto mt-14 px-14 md:mt-40 md:px-24">
<div className="items-start gap-54 md:flex">
<PersonPageAside data={data} />
<main className="flex-auto @container max-md:mt-34">
<TitlePageHeaderLayout name={person.name} />
<Biography person={person} />
<AdHost slot="person_top" className="pt-48" />
<KnowForList items={knownFor} />
<PersonPageCredits data={data} />
</main>
</div>
</div>
</Fragment>
);
}
interface BiographyProps {
person: Person;
}
function Biography({person}: BiographyProps) {
if (!person.description) return null;
return (
<Fragment>
<SiteSectionHeading fontSize="text-xl">
<Trans message="Biography" />
</SiteSectionHeading>
<TruncatedDescription
className="text-sm"
description={person.description}
/>
</Fragment>
);
}
interface KnownForProps {
items: PersonCredit[];
}
function KnowForList({items}: KnownForProps) {
if (!items?.length) return null;
return (
<div className="mt-34">
<SiteSectionHeading fontSize="text-xl">
<Trans message="Known for" />
</SiteSectionHeading>
<ContentGridLayout variant="portrait">
{items.slice(0, 4).map(item => (
<TitlePortraitGridItem
key={item.id}
item={item}
description={
<CharacterOrJob className="text-muted" credit={item} />
}
/>
))}
</ContentGridLayout>
</div>
);
}