From e7e49f81ccd34a0b4cc4e1a7938734f060da7d28 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:26:21 -0700 Subject: [PATCH] add similar media carousel to details modal --- src/assets/locales/en.json | 1 + .../carousels/SimilarMediaCarousel.tsx | 126 ++++++++++++++++++ .../components/layout/DetailsContent.tsx | 13 ++ 3 files changed, 140 insertions(+) create mode 100644 src/components/overlays/detailsModal/components/carousels/SimilarMediaCarousel.tsx diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 701ccfaf..fd1d56f0 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -423,6 +423,7 @@ "airs": "Airs", "endsAt": "Ends at {{time}}", "trailer": "Trailer", + "similar": "Similar", "collection": { "movies": "Movies", "movie": "Movie", diff --git a/src/components/overlays/detailsModal/components/carousels/SimilarMediaCarousel.tsx b/src/components/overlays/detailsModal/components/carousels/SimilarMediaCarousel.tsx new file mode 100644 index 00000000..81c96223 --- /dev/null +++ b/src/components/overlays/detailsModal/components/carousels/SimilarMediaCarousel.tsx @@ -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([]); + const carouselRef = useRef(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 ( +
+

+ {t("details.similar")} +

+ +
+ {/* Carousel Container */} +
+
+ + {similarMedia.map((media) => ( +
+ +
+ ))} + +
+
+ + {/* Navigation Buttons */} + {!isMobile && ( + + )} +
+
+ ); +} diff --git a/src/components/overlays/detailsModal/components/layout/DetailsContent.tsx b/src/components/overlays/detailsModal/components/layout/DetailsContent.tsx index a1b212f0..2079c6bf 100644 --- a/src/components/overlays/detailsModal/components/layout/DetailsContent.tsx +++ b/src/components/overlays/detailsModal/components/layout/DetailsContent.tsx @@ -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 && ( + + )}
);