Страница медиа
Полная страничка медиа для подробной информации
This commit is contained in:
parent
004980a2cf
commit
561a3426e0
@ -1,378 +1,107 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useMedia } from '../contexts/MediaContext';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { getMediaById } from '../services/supabase';
|
||||
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 [searchParams] = useSearchParams();
|
||||
const mediaType = searchParams.get('type') || 'movie';
|
||||
|
||||
const { fetchMediaDetails, currentMedia, loading } = useMedia();
|
||||
const [media, setMedia] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
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
|
||||
const handleSubmitReview = (reviewData) => {
|
||||
// In a real app, this would send the review to the backend
|
||||
const newReview = {
|
||||
id: Date.now().toString(),
|
||||
user: {
|
||||
id: currentUser.uid,
|
||||
username: currentUser.displayName || 'User',
|
||||
profilePicture: currentUser.photoURL,
|
||||
isCritic: false
|
||||
},
|
||||
...reviewData,
|
||||
likes: 0,
|
||||
comments: []
|
||||
useEffect(() => {
|
||||
const loadMedia = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await getMediaById(id);
|
||||
setMedia(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading media:', err);
|
||||
setError('Не удалось загрузить информацию о медиа');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
setReviews(prevReviews => [newReview, ...prevReviews]);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
|
||||
if (id) {
|
||||
loadMedia();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
return <div className="text-center">Загрузка...</div>;
|
||||
}
|
||||
|
||||
if (!currentMedia) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500">{error}</div>;
|
||||
}
|
||||
|
||||
// Extract media details
|
||||
const {
|
||||
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];
|
||||
|
||||
|
||||
if (!media) {
|
||||
return <div className="text-center">Медиа не найдено</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-16">
|
||||
{/* Hero Section */}
|
||||
<div className="relative w-full h-[500px] md:h-[600px]">
|
||||
{backdropUrl ? (
|
||||
<>
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={backdropUrl}
|
||||
alt={mediaTitle}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-campfire-dark via-campfire-dark/70 to-transparent"></div>
|
||||
</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="container mx-auto px-4 py-8">
|
||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
{/* Заголовок и основная информация */}
|
||||
<div className="p-6">
|
||||
<h1 className="text-3xl font-bold mb-4">{media.title}</h1>
|
||||
<div className="flex flex-wrap gap-4 text-gray-600 mb-4">
|
||||
<span>Тип: {media.type}</span>
|
||||
{media.release_date && (
|
||||
<span>Дата выхода: {new Date(media.release_date).toLocaleDateString()}</span>
|
||||
)}
|
||||
{media.rating && (
|
||||
<span>Рейтинг: {media.rating.toFixed(1)}</span>
|
||||
)}
|
||||
|
||||
<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>
|
||||
{media.description && (
|
||||
<p className="text-gray-700 mb-6">{media.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="container-custom py-12 md:py-24">
|
||||
{/* Tabs Navigation */}
|
||||
<div className="border-b border-campfire-charcoal mb-8">
|
||||
<div className="flex overflow-x-auto space-x-8">
|
||||
<button
|
||||
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>
|
||||
|
||||
{/* Постер и дополнительная информация */}
|
||||
{media.poster_url && (
|
||||
<div className="p-6 border-t">
|
||||
<img
|
||||
src={media.poster_url}
|
||||
alt={media.title}
|
||||
className="max-w-sm mx-auto rounded-lg shadow-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="mb-12">
|
||||
{/* Overview Tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Synopsis</h2>
|
||||
<p className="text-campfire-light mb-8">{overview}</p>
|
||||
|
||||
{/* Cast Section */}
|
||||
{credits.cast && credits.cast.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<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">
|
||||
{credits.cast.slice(0, 6).map(person => (
|
||||
<div key={person.id} className="text-center">
|
||||
<div className="relative w-full aspect-[2/3] mb-2 bg-campfire-charcoal rounded-lg overflow-hidden">
|
||||
{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={32} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-medium text-sm">{person.name}</h3>
|
||||
<p className="text-campfire-ash text-xs">{person.character}</p>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
|
||||
{/* Рецензии */}
|
||||
<div className="p-6 border-t">
|
||||
<h2 className="text-2xl font-bold mb-4">Рецензии</h2>
|
||||
{media.reviews && media.reviews.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{media.reviews.map((review) => (
|
||||
<div key={review.id} className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="flex items-center mb-2">
|
||||
<span className="font-semibold">{review.user.username}</span>
|
||||
{review.user.is_critic && (
|
||||
<span className="ml-2 px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
|
||||
Критик
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-700">{review.content}</p>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{new Date(review.created_at).toLocaleDateString()}
|
||||
</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>
|
||||
) : (
|
||||
<p className="text-gray-500">Пока нет рецензий</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default MediaPage;
|
Loading…
Reference in New Issue
Block a user