257 lines
12 KiB
JavaScript
257 lines
12 KiB
JavaScript
import React, { useState } from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
import { FaFire, FaRegCommentDots, FaEye, FaClock, FaCheckCircle, FaGamepad } from 'react-icons/fa';
|
||
import { getFileUrl } from '../../services/pocketbaseService';
|
||
import DOMPurify from 'dompurify';
|
||
import LikeButton from './LikeButton';
|
||
|
||
// Mapping for watched/completed status values
|
||
const watchedStatusLabels = {
|
||
not_watched: 'Не просмотрено',
|
||
watched: 'Просмотрено',
|
||
};
|
||
|
||
const completedStatusLabels = {
|
||
not_completed: 'Не пройдено',
|
||
completed: 'Пройдено',
|
||
};
|
||
|
||
|
||
// ReviewItem now expects review, media, season, reviewCharacteristics, isProfilePage, and isSmallCard props
|
||
function ReviewItem({ review, media, season, reviewCharacteristics = {}, isProfilePage = false, isSmallCard = false }) {
|
||
const [showFullReview, setShowFullReview] = useState(false);
|
||
const [showSpoilers, setShowSpoilers] = useState(false);
|
||
|
||
const reviewMedia = media || review.expand?.media_id;
|
||
const reviewSeason = season || review.expand?.season_id;
|
||
const reviewUser = review.expand?.user_id;
|
||
|
||
const characteristics = reviewMedia?.characteristics && typeof reviewMedia.characteristics === 'object'
|
||
? reviewMedia.characteristics
|
||
: reviewCharacteristics;
|
||
|
||
const reviewTitle = reviewMedia
|
||
? `${reviewMedia.title}${reviewSeason ? ` - Сезон ${reviewSeason.season_number}${reviewSeason.title ? `: ${reviewSeason.title}` : ''}` : ''}`
|
||
: 'Неизвестное произведение';
|
||
|
||
const reviewLink = reviewMedia ? `/media/${reviewMedia.path}` : '#';
|
||
|
||
|
||
// Sanitize HTML content from the rich text editor
|
||
const sanitizedContent = DOMPurify.sanitize(review.content);
|
||
|
||
|
||
// Determine progress display based on media's progress_type
|
||
const renderProgress = () => {
|
||
if (!reviewMedia || !reviewMedia.progress_type || review.progress === undefined || review.progress === null) {
|
||
return null;
|
||
}
|
||
|
||
const progressType = reviewMedia.progress_type;
|
||
const progressValue = review.progress;
|
||
|
||
if (progressType === 'hours') {
|
||
return (
|
||
<span className="flex items-center text-campfire-ash text-sm">
|
||
<FaClock className="mr-1 text-base" /> {progressValue} часов
|
||
</span>
|
||
);
|
||
} else if (progressType === 'watched') {
|
||
const label = watchedStatusLabels[progressValue] || progressValue;
|
||
return (
|
||
<span className="flex items-center text-campfire-ash text-sm">
|
||
<FaEye className="mr-1 text-base" /> {label}
|
||
</span>
|
||
);
|
||
} else if (progressType === 'completed') {
|
||
const label = completedStatusLabels[progressValue] || progressValue;
|
||
return (
|
||
<span className="flex items-center text-campfire-ash text-sm">
|
||
<FaCheckCircle className="mr-1 text-base" /> {label}
|
||
</span>
|
||
);
|
||
}
|
||
return (
|
||
<span className="flex items-center text-campfire-ash text-sm">
|
||
Прогресс: {progressValue}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
// Render as a small card for the "All Reviews" section on the profile page
|
||
if (isSmallCard) {
|
||
return (
|
||
<Link to={reviewLink} className="bg-campfire-charcoal rounded-lg shadow-md p-4 border border-campfire-ash/20 flex flex-col hover:border-campfire-amber transition-colors duration-200 relative overflow-hidden"> {/* Added relative and overflow-hidden */}
|
||
{/* Small Poster in corner - Only show on profile page small cards */}
|
||
{isProfilePage && reviewMedia?.poster && (
|
||
<div className="absolute top-0 right-0 w-16 h-24 overflow-hidden rounded-tr-lg rounded-bl-lg border-b border-l border-campfire-ash/30"> {/* Adjusted size and positioning */}
|
||
<img
|
||
src={getFileUrl(reviewMedia, 'poster')}
|
||
alt={`Постер ${reviewMedia.title}`}
|
||
className="w-full h-full object-cover" // Ensure image covers the container
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* User Avatar and Title */}
|
||
<div className={`flex items-center mb-3 ${isProfilePage && reviewMedia?.poster ? 'pr-20' : ''}`}> {/* Added right padding conditionally */}
|
||
{reviewUser && (
|
||
<img
|
||
src={getFileUrl(reviewUser, 'profile_picture') || 'https://images.pexels.com/photos/1704488/pexels-photo-1704488.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'}
|
||
alt={reviewUser.username}
|
||
className="w-8 h-8 rounded-full object-cover mr-3 border border-campfire-ash/30"
|
||
/>
|
||
)}
|
||
<h3 className="text-sm font-bold text-campfire-amber leading-tight line-clamp-2 flex-grow">
|
||
{reviewTitle}
|
||
</h3>
|
||
</div>
|
||
|
||
{/* Overall Rating */}
|
||
<div className="flex items-center text-campfire-amber font-bold text-lg mb-3">
|
||
<FaFire className="mr-1 text-base" />
|
||
{review.overall_rating !== null && review.overall_rating !== undefined ? parseFloat(review.overall_rating).toFixed(1) : 'N/A'} / 10
|
||
</div>
|
||
|
||
{/* Progress Display */}
|
||
{renderProgress() && (
|
||
<div className="text-campfire-ash text-xs mb-3">
|
||
{renderProgress()}
|
||
</div>
|
||
)}
|
||
|
||
{/* Snippet of Review Content */}
|
||
<div className="text-campfire-ash text-xs leading-relaxed line-clamp-3 mb-3">
|
||
{/* Strip HTML for small card snippet */}
|
||
{sanitizedContent.replace(/<[^>]*>?/gm, '').substring(0, 150)}{sanitizedContent.replace(/<[^>]*>?/gm, '').length > 150 ? '...' : ''}
|
||
</div>
|
||
|
||
{/* Date */}
|
||
<div className="text-campfire-ash text-xs mt-auto pt-2 border-t border-campfire-ash/20">
|
||
<span>{new Date(review.created).toLocaleDateString()}</span>
|
||
</div>
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
|
||
// Default rendering for larger cards (Showcase, Media Page)
|
||
return (
|
||
<div className="bg-campfire-charcoal rounded-lg shadow-md p-6 border border-campfire-ash/20 flex flex-col">
|
||
{/* Media Poster - Only show on profile page large cards (Showcase) */}
|
||
{isProfilePage && reviewMedia?.poster && (
|
||
<Link to={reviewLink} className="block mb-4 self-center">
|
||
<img
|
||
src={getFileUrl(reviewMedia, 'poster')}
|
||
alt={`Постер ${reviewMedia.title}`}
|
||
className="w-32 h-auto object-cover rounded-md"
|
||
/>
|
||
</Link>
|
||
)}
|
||
|
||
{/* Header: User, Media Title, Overall Rating */}
|
||
<div className="flex items-center justify-between mb-4 border-b border-campfire-ash/20 pb-4">
|
||
<div className="flex items-center">
|
||
{/* User Avatar */}
|
||
{reviewUser && (
|
||
<Link to={`/profile/${reviewUser.username}`} className="flex-shrink-0 mr-4">
|
||
<img
|
||
src={getFileUrl(reviewUser, 'profile_picture') || 'https://images.pexels.com/photos/1704488/pexels-photo-1704488.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'}
|
||
alt={reviewUser.username}
|
||
className="w-10 h-10 rounded-full object-cover border border-campfire-ash/30"
|
||
/>
|
||
</Link>
|
||
)}
|
||
<div>
|
||
{/* User Link */}
|
||
{reviewUser && (
|
||
<Link to={`/profile/${reviewUser.username}`} className="text-campfire-light font-semibold hover:underline text-sm">
|
||
{reviewUser.username}
|
||
</Link>
|
||
)}
|
||
{/* Media Title Link */}
|
||
<h3 className="text-lg font-bold text-campfire-amber leading-tight">
|
||
<Link to={reviewLink} className="hover:underline">
|
||
{reviewTitle}
|
||
</Link>
|
||
</h3>
|
||
</div>
|
||
</div>
|
||
{/* Overall Rating */}
|
||
<div className="flex items-center text-campfire-amber font-bold text-xl flex-shrink-0">
|
||
<FaFire className="mr-1 text-lg" />
|
||
{review.overall_rating !== null && review.overall_rating !== undefined ? parseFloat(review.overall_rating).toFixed(1) : 'N/A'} / 10
|
||
</div>
|
||
</div>
|
||
|
||
{/* Characteristics Ratings */}
|
||
{Object.keys(characteristics).length > 0 && (
|
||
<div className="mb-4">
|
||
<p className="text-campfire-light text-sm font-semibold mb-2">Оценки по характеристикам:</p>
|
||
<div className="grid grid-cols-2 gap-x-4 gap-y-2 text-campfire-ash text-sm">
|
||
{Object.entries(characteristics).map(([key, label]) => {
|
||
const ratingValue = review.ratings?.[key];
|
||
if (typeof ratingValue === 'number' && ratingValue >= 1 && ratingValue <= 10) {
|
||
return (
|
||
<div key={key} className="flex justify-between items-center">
|
||
<span>{label}:</span>
|
||
<span className="flex items-center text-campfire-amber font-bold">
|
||
<FaFire className="mr-1 text-xs" /> {ratingValue}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
return null;
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Review Content and Footer (Flex container) */}
|
||
<div className="flex flex-col flex-grow">
|
||
{/* Review Content */}
|
||
<div className={`text-campfire-ash leading-relaxed mb-4 ${!showFullReview ? 'line-clamp-4' : ''}`}>
|
||
{review.has_spoilers && !showSpoilers ? (
|
||
<div className="bg-status-warning/10 border border-status-warning/20 text-status-warning p-4 rounded-md">
|
||
<p className="font-semibold mb-2">Внимание: Эта рецензия содержит спойлеры!</p>
|
||
<button
|
||
onClick={() => setShowSpoilers(true)}
|
||
className="text-status-warning hover:underline text-sm"
|
||
>
|
||
Показать спойлеры
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
|
||
)}
|
||
</div>
|
||
|
||
{/* Read More Button */}
|
||
{review.content && review.content.length > 300 && (
|
||
<button
|
||
onClick={() => setShowFullReview(!showFullReview)}
|
||
className="text-campfire-amber hover:underline text-sm self-start mb-4"
|
||
>
|
||
{showFullReview ? 'Свернуть' : 'Читать далее'}
|
||
</button>
|
||
)}
|
||
|
||
{/* Footer: Date, Comment Count (Placeholder) and Progress */}
|
||
<div className="flex items-end justify-between text-campfire-ash text-xs mt-auto pt-4 border-t border-campfire-ash/20">
|
||
<div className="flex items-center space-x-4">
|
||
<span>{new Date(review.created).toLocaleDateString()}</span>
|
||
{renderProgress()}
|
||
</div>
|
||
<LikeButton
|
||
reviewId={review.id}
|
||
initialLikes={review.like_count || 0}
|
||
reviewOwnerId={review.user_id}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default ReviewItem;
|