mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-20 15:52:24 +00:00
add trailer carousel to details modal
This commit is contained in:
parent
4d5a5151f1
commit
6997acd71a
4 changed files with 126 additions and 0 deletions
|
|
@ -24,6 +24,8 @@ import {
|
|||
TMDBSeasonMetaResult,
|
||||
TMDBShowData,
|
||||
TMDBShowSearchResult,
|
||||
TMDBVideo,
|
||||
TMDBVideosResponse,
|
||||
} from "./types/tmdb";
|
||||
import { mwFetch } from "../helpers/fetch";
|
||||
|
||||
|
|
@ -511,6 +513,19 @@ export async function getMediaCredits(
|
|||
return get<TMDBCredits>(`/${endpoint}/${id}/credits`);
|
||||
}
|
||||
|
||||
export async function getMediaVideos(
|
||||
id: string,
|
||||
type: TMDBContentTypes,
|
||||
): Promise<TMDBVideo[]> {
|
||||
const endpoint = type === TMDBContentTypes.MOVIE ? "movie" : "tv";
|
||||
const data = await get<TMDBVideosResponse>(`/${endpoint}/${id}/videos`);
|
||||
return data.results.filter(
|
||||
(video) =>
|
||||
video.site === "YouTube" &&
|
||||
(video.type === "Trailer" || video.type === "Teaser"),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getRelatedMedia(
|
||||
id: string,
|
||||
type: TMDBContentTypes,
|
||||
|
|
|
|||
|
|
@ -381,3 +381,19 @@ export interface TMDBPersonImages {
|
|||
id: number;
|
||||
profiles: TMDBPersonImage[];
|
||||
}
|
||||
|
||||
export interface TMDBVideo {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
site: string;
|
||||
size: number;
|
||||
type: string;
|
||||
official: boolean;
|
||||
published_at: string;
|
||||
}
|
||||
|
||||
export interface TMDBVideosResponse {
|
||||
id: number;
|
||||
results: TMDBVideo[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getMediaVideos } from "@/backend/metadata/tmdb";
|
||||
import { TMDBContentTypes, TMDBVideo } from "@/backend/metadata/types/tmdb";
|
||||
|
||||
interface TrailerCarouselProps {
|
||||
mediaId: string;
|
||||
mediaType: TMDBContentTypes;
|
||||
onTrailerClick: (videoKey: string) => void;
|
||||
}
|
||||
|
||||
export function TrailerCarousel({
|
||||
mediaId,
|
||||
mediaType,
|
||||
onTrailerClick,
|
||||
}: TrailerCarouselProps) {
|
||||
const { t } = useTranslation();
|
||||
const [videos, setVideos] = useState<TMDBVideo[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadVideos() {
|
||||
try {
|
||||
const mediaVideos = await getMediaVideos(mediaId, mediaType);
|
||||
// Sort by official status and then by type (Trailer first, then Teaser)
|
||||
const sortedVideos = mediaVideos.sort((a, b) => {
|
||||
if (a.official !== b.official) return b.official ? 1 : -1;
|
||||
if (a.type !== b.type) return a.type === "Trailer" ? -1 : 1;
|
||||
return 0;
|
||||
});
|
||||
setVideos(sortedVideos);
|
||||
} catch (err) {
|
||||
console.error("Failed to load videos:", err);
|
||||
}
|
||||
}
|
||||
loadVideos();
|
||||
}, [mediaId, mediaType]);
|
||||
|
||||
if (videos.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 pt-8">
|
||||
<h3 className="text-lg font-semibold text-white/90">
|
||||
{t("details.trailers", "Trailers")}
|
||||
</h3>
|
||||
<div className="flex overflow-x-auto scrollbar-none pb-4 gap-4">
|
||||
{videos.map((video) => (
|
||||
<button
|
||||
key={video.id}
|
||||
type="button"
|
||||
onClick={() => onTrailerClick(video.key)}
|
||||
className="flex-shrink-0 hover:opacity-80 transition-opacity rounded-lg overflow-hidden"
|
||||
>
|
||||
<div className="relative h-52 w-96 overflow-hidden bg-black/60">
|
||||
<img
|
||||
src={`https://img.youtube.com/vi/${video.key}/hqdefault.jpg`}
|
||||
alt={video.name}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-transparent to-transparent" />
|
||||
<div className="absolute top-3 left-3 right-3">
|
||||
<h4 className="text-white font-medium text-sm leading-tight line-clamp-2 text-left">
|
||||
{video.name}
|
||||
</h4>
|
||||
{/* <p className="text-white/80 text-xs mt-1">{video.type}</p> */}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</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 { TrailerCarousel } from "../carousels/TrailerCarousel";
|
||||
import { CollectionOverlay } from "../overlays/CollectionOverlay";
|
||||
import { TrailerOverlay } from "../overlays/TrailerOverlay";
|
||||
import { DetailsBody } from "../sections/DetailsBody";
|
||||
|
|
@ -367,6 +368,26 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Trailer Carousel */}
|
||||
{data.id && (
|
||||
<TrailerCarousel
|
||||
mediaId={data.id.toString()}
|
||||
mediaType={
|
||||
data.type === "movie"
|
||||
? TMDBContentTypes.MOVIE
|
||||
: TMDBContentTypes.TV
|
||||
}
|
||||
onTrailerClick={(videoKey) => {
|
||||
const trailerUrl = `https://www.youtube.com/embed/${videoKey}?autoplay=1&rel=0`;
|
||||
setShowTrailer(true);
|
||||
setImdbData((prev: any) => ({
|
||||
...prev,
|
||||
trailer_url: trailerUrl,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue