147 lines
6.1 KiB
TypeScript
147 lines
6.1 KiB
TypeScript
|
|
|
||
|
|
import React, { useState, useRef } from 'react';
|
||
|
|
import { Post, CurrentView } from '../types';
|
||
|
|
import { PlayIcon, PauseIcon } from './icons';
|
||
|
|
import { UI_TEXTS } from '../constants';
|
||
|
|
import { ViewMode } from './FilterSortBar';
|
||
|
|
|
||
|
|
interface PostCardProps {
|
||
|
|
post: Post;
|
||
|
|
isFeatured?: boolean;
|
||
|
|
setCurrentView: (view: CurrentView) => void;
|
||
|
|
setSelectedItemId: (id: string | null) => void;
|
||
|
|
viewMode?: ViewMode;
|
||
|
|
}
|
||
|
|
|
||
|
|
const PostCard: React.FC<PostCardProps> = ({ post, isFeatured = false, setCurrentView, setSelectedItemId, viewMode = 'grid' }) => {
|
||
|
|
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
|
||
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||
|
|
|
||
|
|
const toggleVideoPlay = (e: React.MouseEvent) => {
|
||
|
|
e.stopPropagation(); // Prevent card click event when clicking video button
|
||
|
|
if (videoRef.current) {
|
||
|
|
if (videoRef.current.paused) {
|
||
|
|
videoRef.current.play();
|
||
|
|
setIsVideoPlaying(true);
|
||
|
|
} else {
|
||
|
|
videoRef.current.pause();
|
||
|
|
setIsVideoPlaying(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCardClick = () => {
|
||
|
|
setCurrentView('blogPostDetail');
|
||
|
|
setSelectedItemId(post.id);
|
||
|
|
};
|
||
|
|
|
||
|
|
if (viewMode === 'list' && !isFeatured) {
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
onClick={handleCardClick}
|
||
|
|
className="group block p-4 rounded-lg bg-white transition-all duration-300 ease-in-out hover:bg-slate-50 cursor-pointer w-full"
|
||
|
|
role="article"
|
||
|
|
aria-labelledby={`post-title-${post.id}`}
|
||
|
|
>
|
||
|
|
<div className="flex items-start space-x-4">
|
||
|
|
<div className="flex-shrink-0 w-24 h-24 md:w-28 md:h-28 rounded-md overflow-hidden bg-slate-200">
|
||
|
|
<img
|
||
|
|
src={post.imageUrl}
|
||
|
|
alt={post.title}
|
||
|
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 ease-in-out"
|
||
|
|
loading="lazy"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div className="flex-1">
|
||
|
|
<h3 id={`post-title-${post.id}`} className="text-base md:text-lg font-quicksand font-semibold text-slate-800 group-hover:text-black mb-1 transition-colors duration-200 leading-tight">
|
||
|
|
{post.title}
|
||
|
|
</h3>
|
||
|
|
<div className="font-inter text-xs md:text-sm text-slate-500 mb-2">
|
||
|
|
<span className="font-semibold text-slate-600">{post.category}</span>
|
||
|
|
{(post.date || post.readTime) && <span className="mx-1.5">•</span>}
|
||
|
|
{post.date && <span>{post.date}</span>}
|
||
|
|
{post.date && post.readTime && <span className="mx-1.5">•</span>}
|
||
|
|
{post.readTime && <span>{post.readTime}</span>}
|
||
|
|
</div>
|
||
|
|
{post.description && (
|
||
|
|
<p className="text-sm text-slate-600 line-clamp-2 font-inter">{post.description}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Grid View (or Featured Card)
|
||
|
|
const imageAspectRatio = isFeatured ? "aspect-[4/5] md:aspect-[16/9]" : "aspect-[1/1]";
|
||
|
|
const titleSize = isFeatured ? "text-2xl md:text-3xl lg:text-4xl" : "text-xl md:text-lg";
|
||
|
|
const scaleEffect = isFeatured ? "group-hover:scale-[1.0125]" : "group-hover:scale-[1.025]";
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={`group relative rounded-xl overflow-hidden transition-all duration-300 bg-white ${isFeatured ? 'flex flex-col' : ''} cursor-pointer shadow-sm hover:shadow-md`}
|
||
|
|
onClick={handleCardClick}
|
||
|
|
role="article"
|
||
|
|
aria-labelledby={`post-title-${post.id}`}
|
||
|
|
>
|
||
|
|
<div className={`relative w-full overflow-hidden ${imageAspectRatio} ${scaleEffect} transition-transform duration-300`}>
|
||
|
|
{post.videoUrl && !isFeatured ? (
|
||
|
|
<>
|
||
|
|
<video
|
||
|
|
ref={videoRef}
|
||
|
|
loop
|
||
|
|
playsInline
|
||
|
|
muted
|
||
|
|
className="absolute inset-0 w-full h-full object-cover"
|
||
|
|
poster={post.imageUrl}
|
||
|
|
onPlay={() => setIsVideoPlaying(true)}
|
||
|
|
onPause={() => setIsVideoPlaying(false)}
|
||
|
|
onClick={(e) => e.stopPropagation()} // Prevent card click on video itself
|
||
|
|
>
|
||
|
|
<source src={post.videoUrl} type="video/mp4" />
|
||
|
|
Your browser does not support the video tag.
|
||
|
|
</video>
|
||
|
|
<button
|
||
|
|
onClick={toggleVideoPlay}
|
||
|
|
className="absolute top-2 right-2 z-10 p-2 bg-black bg-opacity-50 rounded-full text-white hover:bg-opacity-75 transition-opacity"
|
||
|
|
aria-label={isVideoPlaying ? UI_TEXTS.pauseVideoAriaLabel : UI_TEXTS.playVideoAriaLabel}
|
||
|
|
>
|
||
|
|
{isVideoPlaying ? <PauseIcon className="w-5 h-5" /> : <PlayIcon className="w-5 h-5" />}
|
||
|
|
</button>
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<img
|
||
|
|
src={post.imageUrl}
|
||
|
|
alt={post.title}
|
||
|
|
className="absolute inset-0 w-full h-full object-cover"
|
||
|
|
loading="lazy"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||
|
|
</div>
|
||
|
|
<div className={`p-4 md:p-6 ${isFeatured ? 'flex-grow flex flex-col justify-between' : ''}`}>
|
||
|
|
<div>
|
||
|
|
<h3 id={`post-title-${post.id}`} className={`font-quicksand font-bold ${titleSize} mb-2 text-slate-800 transition-colors`}>
|
||
|
|
{post.title}
|
||
|
|
</h3>
|
||
|
|
{isFeatured && post.description && (
|
||
|
|
<p className="font-inter text-sm md:text-base text-slate-600 mb-3 line-clamp-3">{post.description}</p>
|
||
|
|
)}
|
||
|
|
{!isFeatured && post.description && (
|
||
|
|
<p className="font-inter text-sm text-slate-600 mb-3 line-clamp-2">{post.description}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="font-inter text-xs md:text-sm text-slate-500">
|
||
|
|
<span className="font-semibold text-slate-600">{post.category}</span>
|
||
|
|
{(post.date || post.readTime) && <span className="mx-1.5">•</span>}
|
||
|
|
{post.date && <span>{post.date}</span>}
|
||
|
|
{post.date && post.readTime && <span className="mx-1.5">•</span>}
|
||
|
|
{post.readTime && <span>{post.readTime}</span>}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default PostCard;
|