mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
add similar media carousel to details modal
This commit is contained in:
parent
6997acd71a
commit
e7e49f81cc
3 changed files with 140 additions and 0 deletions
|
|
@ -423,6 +423,7 @@
|
|||
"airs": "Airs",
|
||||
"endsAt": "Ends at {{time}}",
|
||||
"trailer": "Trailer",
|
||||
"similar": "Similar",
|
||||
"collection": {
|
||||
"movies": "Movies",
|
||||
"movie": "Movie",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue