Spaces:
Running
Running
import React, { useEffect, useState } from 'react'; | |
import { useParams, Link } from 'react-router-dom'; | |
import { Play, Plus, ThumbsUp, Share2 } from 'lucide-react'; | |
import { getMovieMetadata, getGenresItems, getMovieCard } from '../lib/api'; | |
import ContentRow from '../components/ContentRow'; | |
import { useToast } from '@/hooks/use-toast'; | |
const MovieDetailPage = () => { | |
const { title } = useParams<{ title: string }>(); | |
const [movie, setMovie] = useState<any>(null); | |
const [loading, setLoading] = useState(true); | |
const [similarMovies, setSimilarMovies] = useState<any[]>([]); | |
const { toast } = useToast(); | |
useEffect(() => { | |
const fetchMovieData = async () => { | |
if (!title) return; | |
try { | |
setLoading(true); | |
const data = await getMovieMetadata(title); | |
setMovie(data); | |
const movieData = data.data; | |
console.log(movieData); | |
// Fetch similar movies based on individual genres | |
if (movieData.genres && movieData.genres.length > 0) { | |
const currentMovieName = movieData.name; | |
const moviesByGenre = await Promise.all( | |
movieData.genres.map(async (genre: any) => { | |
// Pass a single genre name for each call | |
const genreResult = await getGenresItems([genre.name], 'movie', 10, 1); | |
if (genreResult.movies && Array.isArray(genreResult.movies)) { | |
return genreResult.movies.map((movieItem: any) => { | |
const { title: similarTitle } = movieItem; | |
// Skip current movie | |
if (similarTitle === currentMovieName) return null; | |
return { | |
type: 'movie', | |
title: similarTitle, | |
}; | |
}); | |
} | |
return []; | |
}) | |
); | |
// Flatten the array of arrays and remove null results | |
const flattenedMovies = moviesByGenre.flat().filter(Boolean); | |
// Remove duplicates based on the title | |
const uniqueMovies = Array.from( | |
new Map(flattenedMovies.map(movie => [movie.title, movie])).values() | |
); | |
setSimilarMovies(uniqueMovies); | |
} | |
} catch (error) { | |
console.error(`Error fetching movie details for ${title}:`, error); | |
toast({ | |
title: "Error loading movie details", | |
description: "Please try again later", | |
variant: "destructive" | |
}); | |
} finally { | |
setLoading(false); | |
} | |
}; | |
fetchMovieData(); | |
}, [title, toast]); | |
if (loading) { | |
return ( | |
<div className="flex items-center justify-center min-h-screen"> | |
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-netflix-red"></div> | |
</div> | |
); | |
} | |
if (!movie) { | |
return ( | |
<div className="pt-24 px-4 md:px-8 text-center min-h-screen"> | |
<h1 className="text-3xl font-bold mb-4">Movie Not Found</h1> | |
<p className="text-netflix-gray mb-6">We couldn't find the movie you're looking for.</p> | |
<Link to="/movies" className="bg-netflix-red px-6 py-2 rounded font-medium"> | |
Back to Movies | |
</Link> | |
</div> | |
); | |
} | |
// Use movieData fields from the new structure | |
const movieData = movie.data; | |
const runtime = movieData.runtime | |
? `${Math.floor(movieData.runtime / 60)}h ${movieData.runtime % 60}m` | |
: ''; | |
const releaseYear = movieData.year || ''; | |
const movieName = (movieData.translations?.nameTranslations?.find((t: any) => t.language === 'eng')?.name || movieData.name || ''); | |
const overview = | |
movieData.overview || | |
(movieData.translations?.overviewTranslations?.find((t: any) => t.language === 'eng')?.overview || ''); | |
return ( | |
<div className="pb-12 animate-fade-in"> | |
{/* Hero backdrop */} | |
<div className="relative w-full h-[500px] md:h-[600px]"> | |
<div className="absolute inset-0"> | |
<img | |
src={movieData.image} | |
alt={movieName} | |
className="w-full h-full object-cover" | |
onError={(e) => { | |
const target = e.target as HTMLImageElement; | |
target.src = '/placeholder.svg'; | |
}} | |
/> | |
<div className="absolute inset-0 bg-gradient-to-t from-netflix-black via-netflix-black/60 to-transparent" /> | |
<div className="absolute inset-0 bg-gradient-to-r from-netflix-black/80 via-netflix-black/40 to-transparent" /> | |
</div> | |
</div> | |
{/* Movie details */} | |
<div className="px-4 md:px-8 -mt-60 relative z-10 max-w-7xl mx-auto"> | |
<div className="flex flex-col md:flex-row gap-8"> | |
{/* Poster */} | |
<div className="flex-shrink-0 hidden md:block"> | |
<img | |
src={movieData.image} | |
alt={movieName} | |
className="w-64 h-96 object-cover rounded-md shadow-lg" | |
onError={(e) => { | |
const target = e.target as HTMLImageElement; | |
target.src = '/placeholder.svg'; | |
}} | |
/> | |
</div> | |
{/* Details */} | |
<div className="flex-grow"> | |
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-3">{movieName}</h1> | |
<div className="flex flex-wrap items-center text-sm text-gray-300 mb-6"> | |
{releaseYear && <span className="mr-3">{releaseYear}</span>} | |
{runtime && <span className="mr-3">{runtime}</span>} | |
{movieData.contentRatings && movieData.contentRatings.length > 0 && ( | |
<span className="mr-3 bg-netflix-red/80 px-2 py-0.5 rounded text-xs"> | |
{movieData.contentRatings[0].name}+ | |
</span> | |
)} | |
</div> | |
<div className="flex flex-wrap items-center gap-2 my-4"> | |
{movieData.genres && | |
movieData.genres.map((genre: any) => ( | |
<Link | |
key={genre.id} | |
to={`/movies?genre=${genre.name}`} | |
className="px-3 py-1 bg-netflix-gray/20 rounded-full text-sm hover:bg-netflix-gray/40 transition" | |
> | |
{genre.name} | |
</Link> | |
))} | |
</div> | |
<p className="text-gray-300 mb-8 max-w-3xl">{overview}</p> | |
<div className="flex flex-wrap gap-3 mb-8"> | |
<Link | |
to={`/movie/${encodeURIComponent(title!)}/watch`} | |
className="flex items-center px-6 py-2 rounded bg-netflix-red text-white font-semibold hover:bg-red-700 transition" | |
> | |
<Play className="w-5 h-5 mr-2" /> Play | |
</Link> | |
<button className="flex items-center px-4 py-2 rounded bg-gray-700 text-white hover:bg-gray-600 transition"> | |
<Plus className="w-5 h-5 mr-2" /> My List | |
</button> | |
<button className="flex items-center justify-center w-10 h-10 rounded-full bg-gray-700 text-white hover:bg-gray-600 transition"> | |
<ThumbsUp className="w-5 h-5" /> | |
</button> | |
<button className="flex items-center justify-center w-10 h-10 rounded-full bg-gray-700 text-white hover:bg-gray-600 transition"> | |
<Share2 className="w-5 h-5" /> | |
</button> | |
</div> | |
{/* Additional details */} | |
<div className="mb-6"> | |
{movieData.translations?.nameTranslations?.find((t: any) => t.isPrimary) && ( | |
<p className="text-gray-400 italic mb-4"> | |
"{movieData.translations.nameTranslations.find((t: any) => t.isPrimary).tagline}" | |
</p> | |
)} | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
{movieData.production_companies && movieData.production_companies.length > 0 && ( | |
<div> | |
<h3 className="text-gray-400 font-semibold mb-1">Production</h3> | |
<p className="text-white"> | |
{movieData.production_companies.map((company: any) => company.name).join(', ')} | |
</p> | |
</div> | |
)} | |
{movieData.spoken_languages && movieData.spoken_languages.length > 0 && ( | |
<div> | |
<h3 className="text-gray-400 font-semibold mb-1">Languages</h3> | |
<p className="text-white">{movieData.spoken_languages.join(', ')}</p> | |
</div> | |
)} | |
</div> | |
</div> | |
</div> | |
</div> | |
{/* Similar Movies */} | |
{similarMovies.length > 0 && ( | |
<div className="mt-16"> | |
<ContentRow title="More Like This" items={similarMovies} /> | |
</div> | |
)} | |
</div> | |
</div> | |
); | |
}; | |
export default MovieDetailPage; | |