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

Полная страничка медиа для подробной информации
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 { 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;