Страница медиа

Полная страничка медиа для подробной информации
This commit is contained in:
degradin 2025-05-07 13:37:25 +03:00
parent 004980a2cf
commit 561a3426e0

View File

@ -1,378 +1,107 @@
import { useState, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useMedia } from '../contexts/MediaContext'; import { getMediaById } from '../services/supabase';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { FaStar, FaCalendar, FaClock, FaUser } from 'react-icons/fa';
import { getImageUrl } from '../services/tmdbApi';
import ReviewForm from '../components/reviews/ReviewForm';
import ReviewCard from '../components/reviews/ReviewCard';
import MediaCarousel from '../components/media/MediaCarousel';
function MediaPage() { const MediaPage = () => {
const { id } = useParams(); const { id } = useParams();
const [searchParams] = useSearchParams(); const [media, setMedia] = useState(null);
const mediaType = searchParams.get('type') || 'movie'; const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const { fetchMediaDetails, currentMedia, loading } = useMedia();
const { currentUser } = useAuth(); const { currentUser } = useAuth();
const [activeTab, setActiveTab] = useState('overview');
const [reviews, setReviews] = useState([]);
useEffect(() => {
if (id && mediaType) {
fetchMediaDetails(id, mediaType);
window.scrollTo(0, 0);
}
}, [id, mediaType, fetchMediaDetails]);
// Mock submit review function useEffect(() => {
const handleSubmitReview = (reviewData) => { const loadMedia = async () => {
// In a real app, this would send the review to the backend try {
const newReview = { setLoading(true);
id: Date.now().toString(), setError(null);
user: { const data = await getMediaById(id);
id: currentUser.uid, setMedia(data);
username: currentUser.displayName || 'User', } catch (err) {
profilePicture: currentUser.photoURL, console.error('Error loading media:', err);
isCritic: false setError('Не удалось загрузить информацию о медиа');
}, } finally {
...reviewData, setLoading(false);
likes: 0, }
comments: []
}; };
setReviews(prevReviews => [newReview, ...prevReviews]); if (id) {
return Promise.resolve(); loadMedia();
}; }
}, [id]);
if (loading) { if (loading) {
return ( return <div className="text-center">Загрузка...</div>;
<div className="pt-20 flex justify-center items-center h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-campfire-amber"></div>
</div>
);
} }
if (!currentMedia) { if (error) {
return ( return <div className="text-red-500">{error}</div>;
<div className="pt-20 container-custom py-16 text-center">
<h1 className="text-3xl font-bold mb-4">Media Not Found</h1>
<p className="text-campfire-ash">The requested media could not be found.</p>
</div>
);
} }
// Extract media details if (!media) {
const { return <div className="text-center">Медиа не найдено</div>;
backdrop_path, }
poster_path,
title,
name,
overview,
vote_average,
release_date,
first_air_date,
runtime,
episode_run_time,
genres = [],
credits = { cast: [], crew: [] },
videos = { results: [] },
similar = { results: [] },
recommendations = { results: [] }
} = currentMedia;
const mediaTitle = title || name;
const releaseDate = release_date || first_air_date;
const duration = runtime || (episode_run_time && episode_run_time[0]);
const backdropUrl = getImageUrl(backdrop_path, 'original');
const posterUrl = getImageUrl(poster_path, 'w342');
// Format release date
const formattedDate = releaseDate ? new Date(releaseDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}) : 'Unknown';
// Format duration
const formattedDuration = duration ? `${Math.floor(duration / 60)}h ${duration % 60}m` : 'Unknown';
// Get trailer
const trailer = videos.results.find(video => video.type === 'Trailer') || videos.results[0];
return ( return (
<div className="pt-16"> <div className="container mx-auto px-4 py-8">
{/* Hero Section */} <div className="bg-white rounded-lg shadow-lg overflow-hidden">
<div className="relative w-full h-[500px] md:h-[600px]"> {/* Заголовок и основная информация */}
{backdropUrl ? ( <div className="p-6">
<> <h1 className="text-3xl font-bold mb-4">{media.title}</h1>
<div className="absolute inset-0"> <div className="flex flex-wrap gap-4 text-gray-600 mb-4">
<img <span>Тип: {media.type}</span>
src={backdropUrl} {media.release_date && (
alt={mediaTitle} <span>Дата выхода: {new Date(media.release_date).toLocaleDateString()}</span>
className="w-full h-full object-cover" )}
/> {media.rating && (
<div className="absolute inset-0 bg-gradient-to-t from-campfire-dark via-campfire-dark/70 to-transparent"></div> <span>Рейтинг: {media.rating.toFixed(1)}</span>
</div>
</>
) : (
<div className="absolute inset-0 bg-campfire-charcoal"></div>
)}
<div className="container-custom relative h-full flex items-end pb-12">
<div className="flex flex-col md:flex-row items-center md:items-end gap-8">
{posterUrl && (
<div className="w-48 md:w-64 flex-shrink-0 rounded-lg overflow-hidden shadow-xl transform md:translate-y-16">
<img
src={posterUrl}
alt={mediaTitle}
className="w-full h-auto"
/>
</div>
)} )}
<div className="text-center md:text-left">
<h1 className="text-3xl md:text-5xl font-bold mb-2">{mediaTitle}</h1>
<div className="flex flex-wrap items-center justify-center md:justify-start gap-4 mb-4 text-sm">
{releaseDate && (
<div className="flex items-center text-campfire-ash">
<FaCalendar className="mr-1" />
<span>{formattedDate}</span>
</div>
)}
{duration && (
<div className="flex items-center text-campfire-ash">
<FaClock className="mr-1" />
<span>{formattedDuration}</span>
</div>
)}
{vote_average > 0 && (
<div className="flex items-center text-campfire-amber">
<FaStar className="mr-1" />
<span>{(vote_average / 2).toFixed(1)}</span>
</div>
)}
</div>
<div className="flex flex-wrap items-center justify-center md:justify-start gap-2 mb-6">
{genres.map(genre => (
<span
key={genre.id}
className="inline-block px-3 py-1 rounded-full text-xs font-medium bg-campfire-charcoal"
>
{genre.name}
</span>
))}
</div>
{trailer && (
<a
href={`https://www.youtube.com/watch?v=${trailer.key}`}
target="_blank"
rel="noopener noreferrer"
className="btn-primary inline-flex items-center"
>
Watch Trailer
</a>
)}
</div>
</div> </div>
{media.description && (
<p className="text-gray-700 mb-6">{media.description}</p>
)}
</div> </div>
</div>
{/* Постер и дополнительная информация */}
{/* Content Section */} {media.poster_url && (
<div className="container-custom py-12 md:py-24"> <div className="p-6 border-t">
{/* Tabs Navigation */} <img
<div className="border-b border-campfire-charcoal mb-8"> src={media.poster_url}
<div className="flex overflow-x-auto space-x-8"> alt={media.title}
<button className="max-w-sm mx-auto rounded-lg shadow-md"
onClick={() => setActiveTab('overview')} />
className={`pb-4 font-medium ${
activeTab === 'overview'
? 'text-campfire-amber border-b-2 border-campfire-amber'
: 'text-campfire-ash hover:text-campfire-light'
}`}
>
Overview
</button>
<button
onClick={() => setActiveTab('reviews')}
className={`pb-4 font-medium ${
activeTab === 'reviews'
? 'text-campfire-amber border-b-2 border-campfire-amber'
: 'text-campfire-ash hover:text-campfire-light'
}`}
>
Reviews
</button>
<button
onClick={() => setActiveTab('similar')}
className={`pb-4 font-medium ${
activeTab === 'similar'
? 'text-campfire-amber border-b-2 border-campfire-amber'
: 'text-campfire-ash hover:text-campfire-light'
}`}
>
Similar
</button>
</div> </div>
</div> )}
{/* Tab Content */} {/* Рецензии */}
<div className="mb-12"> <div className="p-6 border-t">
{/* Overview Tab */} <h2 className="text-2xl font-bold mb-4">Рецензии</h2>
{activeTab === 'overview' && ( {media.reviews && media.reviews.length > 0 ? (
<div> <div className="space-y-4">
<h2 className="text-2xl font-bold mb-4">Synopsis</h2> {media.reviews.map((review) => (
<p className="text-campfire-light mb-8">{overview}</p> <div key={review.id} className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
{/* Cast Section */} <span className="font-semibold">{review.user.username}</span>
{credits.cast && credits.cast.length > 0 && ( {review.user.is_critic && (
<div className="mb-8"> <span className="ml-2 px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
<h2 className="text-2xl font-bold mb-4">Cast</h2> Критик
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4"> </span>
{credits.cast.slice(0, 6).map(person => ( )}
<div key={person.id} className="text-center"> </div>
<div className="relative w-full aspect-[2/3] mb-2 bg-campfire-charcoal rounded-lg overflow-hidden"> <p className="text-gray-700">{review.content}</p>
{person.profile_path ? ( <div className="mt-2 text-sm text-gray-500">
<img {new Date(review.created_at).toLocaleDateString()}
src={getImageUrl(person.profile_path, 'w185')}
alt={person.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<FaUser className="text-campfire-ash" size={32} />
</div>
)}
</div>
<h3 className="font-medium text-sm">{person.name}</h3>
<p className="text-campfire-ash text-xs">{person.character}</p>
</div>
))}
</div> </div>
</div> </div>
)} ))}
{/* Crew Section */}
{credits.crew && credits.crew.length > 0 && (
<div>
<h2 className="text-2xl font-bold mb-4">Crew</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{credits.crew
.filter(person =>
['Director', 'Producer', 'Writer', 'Screenplay'].includes(person.job)
)
.slice(0, 6)
.map(person => (
<div key={`${person.id}-${person.job}`} className="flex items-center">
<div className="w-12 h-12 rounded-full overflow-hidden bg-campfire-charcoal mr-3 flex-shrink-0">
{person.profile_path ? (
<img
src={getImageUrl(person.profile_path, 'w185')}
alt={person.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<FaUser className="text-campfire-ash" size={18} />
</div>
)}
</div>
<div>
<h3 className="font-medium">{person.name}</h3>
<p className="text-campfire-ash text-sm">{person.job}</p>
</div>
</div>
))
}
</div>
</div>
)}
</div>
)}
{/* Reviews Tab */}
{activeTab === 'reviews' && (
<div>
<h2 className="text-2xl font-bold mb-6">Reviews</h2>
{/* Submit Review Form */}
{currentUser ? (
<div className="mb-8">
<ReviewForm
mediaId={id}
mediaType={mediaType}
onSubmit={handleSubmitReview}
/>
</div>
) : (
<div className="bg-campfire-charcoal rounded-lg p-6 mb-8 text-center">
<p className="text-campfire-ash mb-4">Sign in to write a review</p>
<a href="/login" className="btn-primary">
Sign In
</a>
</div>
)}
{/* Reviews List */}
{reviews.length > 0 ? (
<div className="space-y-6">
{reviews.map(review => (
<ReviewCard key={review.id} review={review} isDetailed={true} />
))}
</div>
) : (
<div className="bg-campfire-charcoal rounded-lg p-8 text-center">
<p className="text-campfire-ash">No reviews yet. Be the first to review!</p>
</div>
)}
</div>
)}
{/* Similar Tab */}
{activeTab === 'similar' && (
<div>
{/* Similar Titles */}
{similar.results && similar.results.length > 0 && (
<div className="mb-12">
<h2 className="text-2xl font-bold mb-6">Similar Titles</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{similar.results.slice(0, 10).map(item => (
<MediaCard key={item.id} media={item} type={mediaType} />
))}
</div>
</div>
)}
{/* Recommendations */}
{recommendations.results && recommendations.results.length > 0 && (
<div>
<h2 className="text-2xl font-bold mb-6">Recommendations</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{recommendations.results.slice(0, 10).map(item => (
<MediaCard key={item.id} media={item} type={mediaType} />
))}
</div>
</div>
)}
{(!similar.results || similar.results.length === 0) &&
(!recommendations.results || recommendations.results.length === 0) && (
<div className="bg-campfire-charcoal rounded-lg p-8 text-center">
<p className="text-campfire-ash">No similar content available.</p>
</div>
)}
</div> </div>
) : (
<p className="text-gray-500">Пока нет рецензий</p>
)} )}
</div> </div>
</div> </div>
</div> </div>
); );
} };
export default MediaPage; export default MediaPage;