feat: filter by media type on PersonDetails (#1566)

* feat: add person media type filtering

* feat: adjust filter to apply to cast and crew

* fix: fix layout issue

re #1513

* feat: revert unused server side api changes

* feat: move media type filtering to client side

Updates person details page to filter cast and crew sections by selected media type

re #1513
This commit is contained in:
Joe Lowrance
2025-05-06 14:04:53 -04:00
committed by GitHub
parent 1870e637e4
commit a19dcaf5e5

View File

@@ -7,6 +7,7 @@ import TitleCard from '@app/components/TitleCard';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { CircleStackIcon } from '@heroicons/react/24/solid';
import type { PersonCombinedCreditsResponse } from '@server/interfaces/api/personInterfaces';
import type { PersonDetails as PersonDetailsType } from '@server/models/Person';
import { groupBy } from 'lodash';
@@ -25,9 +26,12 @@ const messages = defineMessages('components.PersonDetails', {
ascharacter: 'as {character}',
});
type MediaType = 'all' | 'movie' | 'tv';
const PersonDetails = () => {
const intl = useIntl();
const router = useRouter();
const [currentMediaType, setCurrentMediaType] = useState<string>('all');
const { data, error } = useSWR<PersonDetailsType>(
`/api/v1/person/${router.query.personId}`
);
@@ -39,7 +43,11 @@ const PersonDetails = () => {
);
const sortedCast = useMemo(() => {
const grouped = groupBy(combinedCredits?.cast ?? [], 'id');
const filtered = (combinedCredits?.cast ?? []).filter(
(media) =>
currentMediaType === 'all' || media.mediaType === currentMediaType
);
const grouped = groupBy(filtered, 'id');
const reduced = Object.values(grouped).map((objs) => ({
...objs[0],
@@ -54,10 +62,14 @@ const PersonDetails = () => {
}
return 1;
});
}, [combinedCredits]);
}, [combinedCredits, currentMediaType]);
const sortedCrew = useMemo(() => {
const grouped = groupBy(combinedCredits?.crew ?? [], 'id');
const filtered = (combinedCredits?.crew ?? []).filter(
(media) =>
currentMediaType === 'all' || media.mediaType === currentMediaType
);
const grouped = groupBy(filtered, 'id');
const reduced = Object.values(grouped).map((objs) => ({
...objs[0],
@@ -72,7 +84,7 @@ const PersonDetails = () => {
}
return 1;
});
}, [combinedCredits]);
}, [combinedCredits, currentMediaType]);
if (!data && !error) {
return <LoadingSpinner />;
@@ -122,6 +134,29 @@ const PersonDetails = () => {
const isLoading = !combinedCredits && !errorCombinedCredits;
const mediaTypePicker = (
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 lg:flex-grow-0">
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
<CircleStackIcon className="h-6 w-6" />
</span>
<select
id="mediaType"
name="mediaType"
onChange={(e) => {
setCurrentMediaType(e.target.value as MediaType);
}}
value={currentMediaType}
className="rounded-r-only"
>
<option value="all">{intl.formatMessage(globalMessages.all)}</option>
<option value="movie">
{intl.formatMessage(globalMessages.movies)}
</option>
<option value="tv">{intl.formatMessage(globalMessages.tvshows)}</option>
</select>
</div>
);
const cast = (sortedCast ?? []).length > 0 && (
<>
<div className="slider-header">
@@ -235,8 +270,11 @@ const PersonDetails = () => {
/>
</div>
)}
<div className="text-center text-gray-300 lg:text-left">
<h1 className="text-3xl text-white lg:text-4xl">{data.name}</h1>
<div className="w-full text-center text-gray-300 lg:text-left">
<div className="flex w-full items-center justify-between">
<h1 className="text-3xl text-white lg:text-4xl">{data.name}</h1>
<div className="flex-shrink-0">{mediaTypePicker}</div>
</div>
<div className="mt-1 mb-2 space-y-1 text-xs text-white sm:text-sm lg:text-base">
<div>{personAttributes.join(' | ')}</div>
{(data.alsoKnownAs ?? []).length > 0 && (