mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(frontend): add crew related movies/shows to person details page
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import type { PersonDetail } from '../../../server/models/Person';
|
import type { PersonDetail } from '../../../server/models/Person';
|
||||||
import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces';
|
import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces';
|
||||||
@@ -11,6 +11,7 @@ import { LanguageContext } from '../../context/LanguageContext';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
appearsin: 'Appears in',
|
appearsin: 'Appears in',
|
||||||
|
crewmember: 'Crew Member',
|
||||||
ascharacter: 'as {character}',
|
ascharacter: 'as {character}',
|
||||||
nobiography: 'No biography available.',
|
nobiography: 'No biography available.',
|
||||||
});
|
});
|
||||||
@@ -22,6 +23,7 @@ const PersonDetails: React.FC = () => {
|
|||||||
const { data, error } = useSWR<PersonDetail>(
|
const { data, error } = useSWR<PersonDetail>(
|
||||||
`/api/v1/person/${router.query.personId}`
|
`/api/v1/person/${router.query.personId}`
|
||||||
);
|
);
|
||||||
|
const [showBio, setShowBio] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: combinedCredits,
|
data: combinedCredits,
|
||||||
@@ -53,77 +55,151 @@ const PersonDetails: React.FC = () => {
|
|||||||
return 1;
|
return 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sortedCrew = combinedCredits?.crew.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;
|
||||||
|
});
|
||||||
|
|
||||||
const isLoading = !combinedCredits && !errorCombinedCredits;
|
const isLoading = !combinedCredits && !errorCombinedCredits;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex mt-8 mb-8 flex-col md:flex-row items-center md:items-start">
|
<div className="flex flex-col items-center mt-8 mb-8 md:flex-row md:items-start">
|
||||||
{data.profilePath && (
|
{data.profilePath && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath})`,
|
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"
|
className="flex-shrink-0 mb-6 mr-0 bg-center bg-cover rounded-full w-36 h-36 md:w-44 md:h-44 md:mb-0 md:mr-6"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="text-gray-300 text-center md:text-left">
|
<div className="text-center text-gray-300 md:text-left">
|
||||||
<h1 className="text-3xl md:text-4xl text-white mb-4">{data.name}</h1>
|
<h1 className="mb-4 text-3xl text-white md:text-4xl">{data.name}</h1>
|
||||||
<div>
|
<div className="relative">
|
||||||
{data.biography
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||||
? data.biography
|
<div
|
||||||
: intl.formatMessage(messages.nobiography)}
|
className={`transition-max-height duration-300 ${
|
||||||
</div>
|
showBio
|
||||||
</div>
|
? 'overflow-visible extra-max-height'
|
||||||
</div>
|
: 'overflow-hidden max-h-44'
|
||||||
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
|
}`}
|
||||||
<div className="flex-1 min-w-0">
|
onClick={() => setShowBio((show) => !show)}
|
||||||
<div className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
|
role="button"
|
||||||
<span>{intl.formatMessage(messages.appearsin)}</span>
|
tabIndex={-1}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-8">
|
|
||||||
{sortedCast?.map((media) => {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={`list-cast-item-${media.id}`}
|
|
||||||
className="col-span-1 flex flex-col text-center items-center"
|
|
||||||
>
|
>
|
||||||
<TitleCard
|
<div className={showBio ? 'h-auto' : 'h-36'}>
|
||||||
id={media.id}
|
{data.biography
|
||||||
title={media.mediaType === 'movie' ? media.title : media.name}
|
? data.biography
|
||||||
userScore={media.voteAverage}
|
: intl.formatMessage(messages.nobiography)}
|
||||||
year={
|
</div>
|
||||||
media.mediaType === 'movie'
|
{!showBio && (
|
||||||
? media.releaseDate
|
<div className="absolute bottom-0 left-0 right-0 w-full h-8 bg-gradient-to-t from-gray-900" />
|
||||||
: media.firstAirDate
|
|
||||||
}
|
|
||||||
image={media.posterPath}
|
|
||||||
summary={media.overview}
|
|
||||||
mediaType={media.mediaType as 'movie' | 'tv'}
|
|
||||||
status={media.mediaInfo?.status}
|
|
||||||
canExpand
|
|
||||||
/>
|
|
||||||
{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>
|
|
||||||
)}
|
)}
|
||||||
</li>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
</div>
|
||||||
{isLoading &&
|
</div>
|
||||||
[...Array(20)].map((_item, i) => (
|
{(sortedCast ?? []).length > 0 && (
|
||||||
<li
|
<>
|
||||||
key={`placeholder-${i}`}
|
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
|
||||||
className="col-span-1 flex flex-col text-center items-center"
|
<div className="flex-1 min-w-0">
|
||||||
>
|
<div className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
|
||||||
<TitleCard.Placeholder canExpand />
|
<span>{intl.formatMessage(messages.appearsin)}</span>
|
||||||
</li>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
|
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-8">
|
||||||
|
{sortedCast?.map((media, index) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`list-cast-item-${media.id}-${index}`}
|
||||||
|
className="flex flex-col items-center col-span-1 text-center"
|
||||||
|
>
|
||||||
|
<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}
|
||||||
|
canExpand
|
||||||
|
/>
|
||||||
|
{media.character && (
|
||||||
|
<div className="mt-2 text-xs text-center text-gray-300 truncate w-36 sm:w-36 md:w-44">
|
||||||
|
{intl.formatMessage(messages.ascharacter, {
|
||||||
|
character: media.character,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(sortedCrew ?? []).length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
|
||||||
|
<span>{intl.formatMessage(messages.crewmember)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-8">
|
||||||
|
{sortedCrew?.map((media, index) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`list-crew-item-${media.id}-${index}`}
|
||||||
|
className="flex flex-col items-center col-span-1 text-center"
|
||||||
|
>
|
||||||
|
<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}
|
||||||
|
canExpand
|
||||||
|
/>
|
||||||
|
{media.job && (
|
||||||
|
<div className="mt-2 text-xs text-center text-gray-300 truncate w-36 sm:w-36 md:w-44">
|
||||||
|
{media.job}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isLoading && <LoadingSpinner />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -7,7 +7,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plex-button {
|
.plex-button {
|
||||||
@apply w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 transition ease-in-out duration-150 text-center disabled:opacity-50;
|
@apply flex justify-center w-full px-4 py-2 text-sm font-medium text-center text-white transition duration-150 ease-in-out bg-indigo-600 border border-transparent rounded-md disabled:opacity-50;
|
||||||
background-color: #cc7b19;
|
background-color: #cc7b19;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.titleCard {
|
.titleCard {
|
||||||
@apply relative bg-cover rounded-lg bg-gray-800;
|
@apply relative bg-gray-800 bg-cover rounded-lg;
|
||||||
padding-bottom: 150%;
|
padding-bottom: 150%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
@apply flex items-center justify-center text-center text-gray-300 relative top-0 left-0 bottom-0 right-0 h-screen flex-col;
|
@apply relative top-0 bottom-0 left-0 right-0 flex flex-col items-center justify-center h-screen text-center text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Used for animating height */
|
||||||
|
.extra-max-height {
|
||||||
|
max-height: 100rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
@@ -49,5 +54,5 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@apply bg-gray-800 py-1 px-2 rounded-md;
|
@apply px-2 py-1 bg-gray-800 rounded-md;
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,9 @@ module.exports = {
|
|||||||
purge: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
|
purge: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
transitionProperty: {
|
||||||
|
'max-height': 'max-height',
|
||||||
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter var', ...defaultTheme.fontFamily.sans],
|
sans: ['Inter var', ...defaultTheme.fontFamily.sans],
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user