Страница медиа
Полная страничка медиа для подробной информации
This commit is contained in:
parent
004980a2cf
commit
561a3426e0
@ -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(() => {
|
useEffect(() => {
|
||||||
if (id && mediaType) {
|
const loadMedia = async () => {
|
||||||
fetchMediaDetails(id, mediaType);
|
try {
|
||||||
window.scrollTo(0, 0);
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const data = await getMediaById(id);
|
||||||
|
setMedia(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading media:', err);
|
||||||
|
setError('Не удалось загрузить информацию о медиа');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [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: []
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
|
{media.rating && (
|
||||||
|
<span>Рейтинг: {media.rating.toFixed(1)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{media.description && (
|
||||||
|
<p className="text-gray-700 mb-6">{media.description}</p>
|
||||||
|
)}
|
||||||
|
</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">
|
{media.poster_url && (
|
||||||
{posterUrl && (
|
<div className="p-6 border-t">
|
||||||
<div className="w-48 md:w-64 flex-shrink-0 rounded-lg overflow-hidden shadow-xl transform md:translate-y-16">
|
|
||||||
<img
|
<img
|
||||||
src={posterUrl}
|
src={media.poster_url}
|
||||||
alt={mediaTitle}
|
alt={media.title}
|
||||||
className="w-full h-auto"
|
className="max-w-sm mx-auto rounded-lg shadow-md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-center md:text-left">
|
{/* Рецензии */}
|
||||||
<h1 className="text-3xl md:text-5xl font-bold mb-2">{mediaTitle}</h1>
|
<div className="p-6 border-t">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Рецензии</h2>
|
||||||
<div className="flex flex-wrap items-center justify-center md:justify-start gap-4 mb-4 text-sm">
|
{media.reviews && media.reviews.length > 0 ? (
|
||||||
{releaseDate && (
|
<div className="space-y-4">
|
||||||
<div className="flex items-center text-campfire-ash">
|
{media.reviews.map((review) => (
|
||||||
<FaCalendar className="mr-1" />
|
<div key={review.id} className="bg-gray-50 p-4 rounded-lg">
|
||||||
<span>{formattedDate}</span>
|
<div className="flex items-center mb-2">
|
||||||
</div>
|
<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">
|
||||||
{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>
|
</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>
|
||||||
|
<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>
|
||||||
</div>
|
</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>
|
|
||||||
</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>
|
|
||||||
</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>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-campfire-charcoal rounded-lg p-8 text-center">
|
<p className="text-gray-500">Пока нет рецензий</p>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default MediaPage;
|
export default MediaPage;
|
Loading…
Reference in New Issue
Block a user