feat(frontend/api): cast included with movie request and cast list on detail page

pull/96/head
sct 4 years ago
parent 6398e3645a
commit 04252f88bb

@ -99,6 +99,27 @@ interface TmdbSearchTvResponse extends TmdbPaginatedResponse {
results: TmdbTvResult[]; results: TmdbTvResult[];
} }
export interface TmdbCreditCast {
cast_id: number;
character: string;
credit_id: string;
gender?: number;
id: number;
name: string;
order: number;
profile_path?: string;
}
export interface TmdbCreditCrew {
credit_id: string;
gender?: number;
id: number;
name: string;
profile_path?: string;
job: string;
department: string;
}
export interface TmdbMovieDetails { export interface TmdbMovieDetails {
id: number; id: number;
imdb_id?: string; imdb_id?: string;
@ -138,6 +159,10 @@ export interface TmdbMovieDetails {
video: boolean; video: boolean;
vote_average: number; vote_average: number;
vote_count: number; vote_count: number;
credits: {
cast: TmdbCreditCast[];
crew: TmdbCreditCrew[];
};
} }
export interface TmdbTvEpisodeDetails { export interface TmdbTvEpisodeDetails {
@ -258,7 +283,7 @@ class TheMovieDb {
const response = await this.axios.get<TmdbMovieDetails>( const response = await this.axios.get<TmdbMovieDetails>(
`/movie/${movieId}`, `/movie/${movieId}`,
{ {
params: { language }, params: { language, append_to_response: 'credits' },
} }
); );

@ -1,7 +1,32 @@
import { TmdbMovieDetails } from '../api/themoviedb'; import {
TmdbMovieDetails,
TmdbCreditCast,
TmdbCreditCrew,
} from '../api/themoviedb';
import { MediaRequest } from '../entity/MediaRequest'; import { MediaRequest } from '../entity/MediaRequest';
import { ProductionCompany, Genre } from './common'; import { ProductionCompany, Genre } from './common';
export interface Cast {
id: number;
castId: number;
character: string;
creditId: string;
gender?: number;
name: string;
order: number;
profilePath?: string;
}
export interface Crew {
id: number;
creditId: string;
department: string;
gender?: number;
job: string;
name: string;
profilePath?: string;
}
export interface MovieDetails { export interface MovieDetails {
id: number; id: number;
imdbId?: string; imdbId?: string;
@ -33,9 +58,34 @@ export interface MovieDetails {
video: boolean; video: boolean;
voteAverage: number; voteAverage: number;
voteCount: number; voteCount: number;
credits: {
cast: Cast[];
crew: Crew[];
};
request?: MediaRequest; request?: MediaRequest;
} }
const mapCast = (person: TmdbCreditCast): Cast => ({
castId: person.cast_id,
character: person.character,
creditId: person.credit_id,
id: person.id,
name: person.name,
order: person.order,
gender: person.gender,
profilePath: person.profile_path,
});
const mapCrew = (person: TmdbCreditCrew): Crew => ({
creditId: person.credit_id,
department: person.department,
id: person.id,
job: person.job,
name: person.name,
gender: person.gender,
profilePath: person.profile_path,
});
export const mapMovieDetails = ( export const mapMovieDetails = (
movie: TmdbMovieDetails, movie: TmdbMovieDetails,
request?: MediaRequest request?: MediaRequest
@ -69,5 +119,9 @@ export const mapMovieDetails = (
posterPath: movie.poster_path, posterPath: movie.poster_path,
runtime: movie.runtime, runtime: movie.runtime,
tagline: movie.tagline, tagline: movie.tagline,
credits: {
cast: movie.credits.cast.map(mapCast),
crew: movie.credits.crew.map(mapCrew),
},
request, request,
}); });

@ -419,6 +419,17 @@ components:
type: number type: number
voteCount: voteCount:
type: number type: number
credits:
type: object
properties:
cast:
type: array
items:
$ref: '#/components/schemas/Cast'
crew:
type: array
items:
$ref: '#/components/schemas/Crew'
request: request:
$ref: '#/components/schemas/MediaRequest' $ref: '#/components/schemas/MediaRequest'
Episode: Episode:
@ -590,6 +601,48 @@ components:
- mediaId - mediaId
- mediaType - mediaType
- status - status
Cast:
type: object
properties:
id:
type: number
example: 123
castId:
type: number
example: 1
character:
type: string
example: Some Character Name
creditId:
type: string
gender:
type: number
name:
type: string
example: Some Persons Name
order:
type: number
profilePath:
type: string
Crew:
type: object
properties:
id:
type: number
example: 123
creditId:
type: string
gender:
type: number
name:
type: string
example: Some Persons Name
job:
type: string
department:
type: string
profilePath:
type: string
securitySchemes: securitySchemes:
cookieAuth: cookieAuth:

@ -6,6 +6,7 @@ import {
} from '../../../../server/models/Search'; } from '../../../../server/models/Search';
import TitleCard from '../../TitleCard'; import TitleCard from '../../TitleCard';
import useVerticalScroll from '../../../hooks/useVerticalScroll'; import useVerticalScroll from '../../../hooks/useVerticalScroll';
import PersonCard from '../../PersonCard';
interface ListViewProps { interface ListViewProps {
items?: (TvResult | MovieResult | PersonResult)[]; items?: (TvResult | MovieResult | PersonResult)[];
@ -64,7 +65,9 @@ const ListView: React.FC<ListViewProps> = ({
); );
break; break;
case 'person': case 'person':
titleCard = <div>{title.name}</div>; titleCard = (
<PersonCard name={title.name} profilePath={title.profilePath} />
);
break; break;
} }

@ -11,6 +11,7 @@ import type { MovieResult } from '../../../server/models/Search';
import Link from 'next/link'; import Link from 'next/link';
import Slider from '../Slider'; import Slider from '../Slider';
import TitleCard from '../TitleCard'; import TitleCard from '../TitleCard';
import PersonCard from '../PersonCard';
interface MovieDetailsProps { interface MovieDetailsProps {
movie?: MovieDetailsType; movie?: MovieDetailsType;
@ -280,6 +281,42 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</div> </div>
</div> </div>
</div> </div>
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="flex-1 min-w-0">
<Link href="/movie/[movieId]/cast" as={`/movie/${data.id}/cast`}>
<a className="inline-flex text-xl leading-7 text-cool-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<span>Cast</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="cast"
isLoading={!data && !error}
isEmpty={false}
items={data?.credits.cast.slice(0, 20).map((person) => (
<PersonCard
key={`cast-item-${person.id}`}
name={person.name}
subName={person.character}
profilePath={person.profilePath}
/>
))}
/>
<div className="md:flex md:items-center md:justify-between mb-4 mt-6"> <div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<Link <Link

@ -0,0 +1,52 @@
import React from 'react';
interface PersonCardProps {
name: string;
subName?: string;
profilePath?: string;
}
const PersonCard: React.FC<PersonCardProps> = ({
name,
subName,
profilePath,
}) => {
return (
<div className="relative w-36 sm:w-36 md:w-44 bg-cool-gray-600 rounded-lg text-white shadow-lg hover:bg-cool-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-cool-gray-300">
{subName}
</div>
)}
</div>
</div>
</div>
);
};
export default PersonCard;

@ -99,7 +99,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
onOk={() => cancelRequest()} onOk={() => cancelRequest()}
/> />
<div <div
className="titleCard outline-none" className="titleCard outline-none cursor-default"
style={{ style={{
backgroundImage: `url(//image.tmdb.org/t/p/w600_and_h900_bestv2${image})`, backgroundImage: `url(//image.tmdb.org/t/p/w600_and_h900_bestv2${image})`,
}} }}
@ -114,7 +114,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
role="link" role="link"
tabIndex={0} tabIndex={0}
> >
<div className="absolute top-0 h-full w-full bottom-0 left-0 right-0 overflow-hidden shadow-md"> <div className="absolute top-0 h-full w-full bottom-0 left-0 right-0 overflow-hidden shadow-xl">
<div <div
className={`absolute left-0 top-0 rounded-tl-md rounded-br-md z-50 ${ className={`absolute left-0 top-0 rounded-tl-md rounded-br-md z-50 ${
mediaType === 'movie' ? 'bg-blue-500' : 'bg-purple-600' mediaType === 'movie' ? 'bg-blue-500' : 'bg-purple-600'
@ -204,7 +204,9 @@ const TitleCard: React.FC<TitleCardProps> = ({
</div> </div>
</div> </div>
<div className="flex justify-between left-0 bottom-0 right-0 top-0 px-2 py-2"> <div className="flex justify-between left-0 bottom-0 right-0 top-0 px-2 py-2">
<Link href={`/movie/${id}`}> <Link
href={mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`}
>
<a className="cursor-pointer flex w-full h-7 text-center text-white bg-indigo-500 rounded-sm mr-1 hover:bg-indigo-400 focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition ease-in-out duration-150"> <a className="cursor-pointer flex w-full h-7 text-center text-white bg-indigo-500 rounded-sm mr-1 hover:bg-indigo-400 focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition ease-in-out duration-150">
<svg <svg
className="w-4 mx-auto" className="w-4 mx-auto"

@ -1,13 +1,11 @@
import React, { useState } from 'react'; import React from 'react';
import { NextPage } from 'next'; import { NextPage } from 'next';
import PlexLoginButton from '../components/PlexLoginButton'; import PersonCard from '../components/PersonCard';
const PlexText: NextPage = () => { const PlexText: NextPage = () => {
const [authToken, setAuthToken] = useState<string>('');
return ( return (
<div> <div>
<PlexLoginButton onAuthToken={(authToken) => setAuthToken(authToken)} /> <PersonCard />
<div className="mt-4">Auth Token: {authToken}</div>
</div> </div>
); );
}; };

Loading…
Cancel
Save