CampFireCritics/src/components/reviews/ReviewItem.jsx
2025-05-21 11:05:20 +03:00

257 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;