add similar media carousel to details modal

This commit is contained in:
Pas 2025-12-01 18:26:21 -07:00
parent 6997acd71a
commit e7e49f81cc
3 changed files with 140 additions and 0 deletions

View file

@ -423,6 +423,7 @@
"airs": "Airs",
"endsAt": "Ends at {{time}}",
"trailer": "Trailer",
"similar": "Similar",
"collection": {
"movies": "Movies",
"movie": "Movie",

View file

@ -0,0 +1,126 @@
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { getMediaPoster, getRelatedMedia } from "@/backend/metadata/tmdb";
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
import { MediaCard } from "@/components/media/MediaCard";
import { useIsMobile } from "@/hooks/useIsMobile";
import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons";
import { useOverlayStack } from "@/stores/interface/overlayStack";
import { MediaItem } from "@/utils/mediaTypes";
interface SimilarMediaCarouselProps {
mediaId: string;
mediaType: TMDBContentTypes;
}
export function SimilarMediaCarousel({
mediaId,
mediaType,
}: SimilarMediaCarouselProps) {
const { t } = useTranslation();
const { isMobile } = useIsMobile();
const { showModal } = useOverlayStack();
const [similarMedia, setSimilarMedia] = useState<MediaItem[]>([]);
const carouselRef = useRef<HTMLDivElement>(null);
const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({
similar: null,
});
useEffect(() => {
const loadSimilarMedia = async () => {
try {
const results = await getRelatedMedia(mediaId, mediaType, 12);
const mediaItems: MediaItem[] = results.map((result) => {
const isMovie = "title" in result;
return {
id: result.id.toString(),
title: isMovie ? result.title : result.name,
poster: getMediaPoster(result.poster_path) || "/placeholder.png",
type: mediaType === TMDBContentTypes.MOVIE ? "movie" : "show",
year: isMovie
? result.release_date
? new Date(result.release_date).getFullYear()
: 0
: result.first_air_date
? new Date(result.first_air_date).getFullYear()
: 0,
release_date: isMovie
? result.release_date
? new Date(result.release_date)
: undefined
: result.first_air_date
? new Date(result.first_air_date)
: undefined,
};
});
setSimilarMedia(mediaItems);
} catch (err) {
console.error("Failed to load similar media:", err);
}
};
loadSimilarMedia();
}, [mediaId, mediaType]);
useEffect(() => {
if (carouselRef.current) {
carouselRefs.current.similar = carouselRef.current;
}
}, []);
const handleShowDetails = (media: MediaItem) => {
showModal("details", {
id: Number(media.id),
type: media.type === "movie" ? "movie" : "show",
});
};
if (similarMedia.length === 0) return null;
return (
<div className="space-y-4 pt-8">
<h3 className="text-lg font-semibold text-white/90">
{t("details.similar")}
</h3>
<div className="relative">
{/* Carousel Container */}
<div
ref={carouselRef}
className="grid grid-flow-col auto-cols-max gap-4 pt-0 overflow-x-scroll scrollbar-none rounded-xl overflow-y-hidden md:pl-8 md:pr-8"
style={{
scrollSnapType: "x mandatory",
scrollBehavior: "smooth",
}}
>
<div className="md:w-12" />
{similarMedia.map((media) => (
<div
key={media.id}
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
style={{ scrollSnapAlign: "start" }}
>
<MediaCard
media={media}
linkable
onShowDetails={handleShowDetails}
/>
</div>
))}
<div className="md:w-12" />
</div>
{/* Navigation Buttons */}
{!isMobile && (
<CarouselNavButtons
categorySlug="similar"
carouselRefs={carouselRefs}
/>
)}
</div>
</div>
);
}

View file

@ -16,6 +16,7 @@ import { scrapeRottenTomatoes } from "@/utils/rottenTomatoesScraper";
import { DetailsContentProps } from "../../types";
import { EpisodeCarousel } from "../carousels/EpisodeCarousel";
import { CastCarousel } from "../carousels/PeopleCarousel";
import { SimilarMediaCarousel } from "../carousels/SimilarMediaCarousel";
import { TrailerCarousel } from "../carousels/TrailerCarousel";
import { CollectionOverlay } from "../overlays/CollectionOverlay";
import { TrailerOverlay } from "../overlays/TrailerOverlay";
@ -388,6 +389,18 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
}}
/>
)}
{/* Similar Media Carousel */}
{data.id && (
<SimilarMediaCarousel
mediaId={data.id.toString()}
mediaType={
data.type === "movie"
? TMDBContentTypes.MOVIE
: TMDBContentTypes.TV
}
/>
)}
</div>
</div>
);