feat: person details page

pull/228/head
sct 4 years ago
parent 02d6b46448
commit d6eb3ae64e

@ -904,6 +904,8 @@ components:
type: string
character:
type: string
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
CreditCrew:
type: object
properties:
@ -958,6 +960,8 @@ components:
type: string
job:
type: string
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
securitySchemes:
cookieAuth:
type: apiKey

@ -0,0 +1,7 @@
import { PersonCreditCast, PersonCreditCrew } from '../../models/Person';
export interface PersonCombinedCreditsResponse {
id: number;
cast: PersonCreditCast[];
crew: PersonCreditCrew[];
}

@ -3,6 +3,7 @@ import {
TmdbPersonCreditCrew,
TmdbPersonDetail,
} from '../api/themoviedb';
import Media from '../entity/Media';
export interface PersonDetail {
id: number;
@ -42,6 +43,7 @@ export interface PersonCredit {
title: string;
adult: boolean;
releaseDate: string;
mediaInfo?: Media;
}
export interface PersonCreditCast extends PersonCredit {
@ -76,7 +78,8 @@ export const mapPersonDetails = (person: TmdbPersonDetail): PersonDetail => ({
});
export const mapCastCredits = (
cast: TmdbPersonCreditCast
cast: TmdbPersonCreditCast,
media?: Media
): PersonCreditCast => ({
id: cast.id,
originalLanguage: cast.original_language,
@ -100,10 +103,12 @@ export const mapCastCredits = (
adult: cast.adult,
releaseDate: cast.release_date,
character: cast.character,
mediaInfo: media,
});
export const mapCrewCredits = (
crew: TmdbPersonCreditCrew
crew: TmdbPersonCreditCrew,
media?: Media
): PersonCreditCrew => ({
id: crew.id,
originalLanguage: crew.original_language,
@ -128,4 +133,5 @@ export const mapCrewCredits = (
releaseDate: crew.release_date,
department: crew.department,
job: crew.job,
mediaInfo: media,
});

@ -1,5 +1,6 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import Media from '../entity/Media';
import logger from '../logger';
import {
mapCastCredits,
@ -32,9 +33,27 @@ personRoutes.get('/:id/combined_credits', async (req, res) => {
language: req.query.language as string,
});
const castMedia = await Media.getRelatedMedia(
combinedCredits.cast.map((result) => result.id)
);
const crewMedia = await Media.getRelatedMedia(
combinedCredits.crew.map((result) => result.id)
);
return res.status(200).json({
cast: combinedCredits.cast.map((result) => mapCastCredits(result)),
crew: combinedCredits.crew.map((result) => mapCrewCredits(result)),
cast: combinedCredits.cast.map((result) =>
mapCastCredits(
result,
castMedia.find((med) => med.tmdbId === result.id)
)
),
crew: combinedCredits.crew.map((result) =>
mapCrewCredits(
result,
crewMedia.find((med) => med.tmdbId === result.id)
)
),
id: combinedCredits.id,
});
});

@ -527,6 +527,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
items={data?.credits.cast.slice(0, 20).map((person) => (
<PersonCard
key={`cast-item-${person.id}`}
personId={person.id}
name={person.name}
subName={person.character}
profilePath={person.profilePath}

@ -1,6 +1,8 @@
import Link from 'next/link';
import React from 'react';
interface PersonCardProps {
personId: number;
name: string;
subName?: string;
profilePath?: string;
@ -8,50 +10,55 @@ interface PersonCardProps {
}
const PersonCard: React.FC<PersonCardProps> = ({
personId,
name,
subName,
profilePath,
canExpand = false,
}) => {
return (
<div
className={`relative ${
canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'
} bg-gray-600 rounded-lg text-white shadow-lg hover:bg-gray-500 transition ease-in-out duration-150 cursor-pointer`}
>
<div style={{ paddingBottom: '150%' }}>
<div className="absolute inset-0 flex flex-col items-center justify-center">
{profilePath && (
<div
style={{
backgroundImage: `url(https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath})`,
}}
className="rounded-full w-28 h-28 md:w-32 md:h-32 bg-cover bg-center mb-6"
/>
)}
{!profilePath && (
<svg
className="w-28 h-28 md:w-32 md:h-32 mb-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z"
clipRule="evenodd"
/>
</svg>
)}
<div className="whitespace-normal text-center">{name}</div>
{subName && (
<div className="whitespace-normal text-center text-sm text-gray-300">
{subName}
<Link href={`/person/${personId}`}>
<a>
<div
className={`relative ${
canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'
} bg-gray-600 rounded-lg text-white shadow-lg hover:bg-gray-500 transition ease-in-out duration-150 cursor-pointer`}
>
<div style={{ paddingBottom: '150%' }}>
<div className="absolute inset-0 flex flex-col items-center justify-center">
{profilePath && (
<div
style={{
backgroundImage: `url(https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath})`,
}}
className="rounded-full w-28 h-28 md:w-32 md:h-32 bg-cover bg-center mb-6"
/>
)}
{!profilePath && (
<svg
className="w-28 h-28 md:w-32 md:h-32 mb-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z"
clipRule="evenodd"
/>
</svg>
)}
<div className="whitespace-normal text-center">{name}</div>
{subName && (
<div className="whitespace-normal text-center text-sm text-gray-300">
{subName}
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
</a>
</Link>
);
};

@ -0,0 +1,120 @@
import { useRouter } from 'next/router';
import React, { useContext } from 'react';
import useSWR from 'swr';
import type { PersonDetail } from '../../../server/models/Person';
import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces';
import Error from '../../pages/_error';
import LoadingSpinner from '../Common/LoadingSpinner';
import Slider from '../Slider';
import TitleCard from '../TitleCard';
import { defineMessages, useIntl } from 'react-intl';
import { LanguageContext } from '../../context/LanguageContext';
const messages = defineMessages({
appearsin: 'Appears in',
ascharacter: 'as {character}',
nobiography: 'No biography available.',
});
const PersonDetails: React.FC = () => {
const intl = useIntl();
const { locale } = useContext(LanguageContext);
const router = useRouter();
const { data, error } = useSWR<PersonDetail>(
`/api/v1/person/${router.query.personId}`
);
const {
data: combinedCredits,
error: errorCombinedCredits,
} = useSWR<PersonCombinedCreditsResponse>(
`/api/v1/person/${router.query.personId}/combined_credits?language=${locale}`
);
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return <Error statusCode={404} />;
}
const sortedCast = combinedCredits?.cast.sort((a, b) => {
const aDate =
a.mediaType === 'movie'
? a.releaseDate?.slice(0, 4) ?? 0
: a.firstAirDate?.slice(0, 4) ?? 0;
const bDate =
b.mediaType === 'movie'
? b.releaseDate?.slice(0, 4) ?? 0
: b.firstAirDate?.slice(0, 4) ?? 0;
if (aDate > bDate) {
return -1;
}
return 1;
});
return (
<>
<div className="flex mt-8 mb-8 flex-col md:flex-row items-center">
{data.profilePath && (
<div
style={{
backgroundImage: `url(https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath})`,
}}
className="rounded-full w-36 h-36 md:w-44 md:h-44 bg-cover bg-center mb-6 md:mb-0 mr-0 md:mr-6 flex-shrink-0"
/>
)}
<div className="text-gray-300 text-center md:text-left">
<h1 className="text-3xl md:text-4xl text-white mb-4">{data.name}</h1>
<div>
{data.biography
? data.biography
: intl.formatMessage(messages.nobiography)}
</div>
</div>
</div>
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="flex-1 min-w-0">
<div className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<span>{intl.formatMessage(messages.appearsin)}</span>
</div>
</div>
</div>
<Slider
isEmpty={!sortedCast}
isLoading={!combinedCredits && !errorCombinedCredits}
sliderKey={`person-${data.id}-slider-cast`}
items={sortedCast?.map((media) => {
return (
<div key={`slider-cast-item-${media.id}`} className="flex flex-col">
<TitleCard
id={media.id}
title={media.mediaType === 'movie' ? media.title : media.name}
userScore={media.voteAverage}
year={
media.mediaType === 'movie'
? media.releaseDate
: media.firstAirDate
}
image={media.posterPath}
summary={media.overview}
mediaType={media.mediaType as 'movie' | 'tv'}
status={media.mediaInfo?.status}
/>
{media.character && (
<div className="mt-2 text-gray-300 text-xs truncate w-36 sm:w-36 md:w-44 text-center">
{intl.formatMessage(messages.ascharacter, {
character: media.character,
})}
</div>
)}
</div>
);
})}
/>
</>
);
};
export default PersonDetails;

@ -478,6 +478,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
items={data?.credits.cast.slice(0, 20).map((person) => (
<PersonCard
key={`cast-item-${person.id}`}
personId={person.id}
name={person.name}
subName={person.character}
profilePath={person.profilePath}

@ -0,0 +1,9 @@
import React from 'react';
import { NextPage } from 'next';
import PersonDetails from '../../../components/PersonDetails';
const MoviePage: NextPage = () => {
return <PersonDetails />;
};
export default MoviePage;
Loading…
Cancel
Save