videofolxtv/client/src/components/video-card.tsx
sebastjanartic 8ce0414679 Add Netflix-style video browsing with categorized rows
Introduces a new `NetflixGrid` component to display videos in categorized rows, similar to Netflix. This includes styling for hidden scrollbars, adding a `className` prop to `VideoCard`, and updating the home page to conditionally render either the `VideoGrid` or the new `NetflixGrid` based on the `viewMode`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 2eb1084e-b728-4449-9231-f1665924c8d5
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/2eb1084e-b728-4449-9231-f1665924c8d5/spLs1aN
2025-08-29 07:09:54 +00:00

113 lines
4.4 KiB
TypeScript

import { Play } from "lucide-react";
import { type Video } from "@shared/schema";
import HLSPreviewThumbnail from "./hls-preview-thumbnail";
interface VideoCardProps {
video: Video;
onClick: (video: Video) => void;
className?: string;
}
function formatDuration(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
function formatViews(views: number): string {
if (views >= 1000000) {
return `${(views / 1000000).toFixed(1)}M views`;
} else if (views >= 1000) {
return `${(views / 1000).toFixed(1)}K views`;
}
return `${views} views`;
}
function formatDate(date: Date | string): string {
const now = new Date();
const createdDate = typeof date === 'string' ? new Date(date) : date;
if (!createdDate || isNaN(createdDate.getTime())) {
return "Unknown";
}
const diffTime = Math.abs(now.getTime() - createdDate.getTime());
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) return "Today";
if (diffDays === 1) return "1 day ago";
if (diffDays < 7) return `${diffDays} days ago`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`;
return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`;
}
export default function VideoCard({ video, onClick, className = "" }: VideoCardProps) {
return (
<div
data-testid={`card-video-${video.id}`}
className={`video-card transition-transform duration-200 hover:scale-[1.02] p-3 ${className}`}
>
{/* Simple thumbnail with fallback - no HLS loading until needed */}
<div
className="relative gradient-card rounded-xl overflow-hidden mb-4 aspect-video cursor-pointer group"
onClick={() => onClick?.(video)}
>
<img
src={video.thumbnailUrl}
alt={video.title}
className="w-full h-full object-cover transition-all duration-300 group-hover:scale-105"
data-testid={`img-thumbnail-${video.id}`}
loading="lazy"
decoding="async"
onError={(e) => {
const target = e.target as HTMLImageElement;
console.log('Thumbnail failed to load:', target.src);
// Show placeholder immediately instead of trying multiple URLs
target.style.display = 'none';
if (target.parentElement && !target.parentElement.querySelector('.thumbnail-placeholder')) {
target.parentElement.style.background = 'linear-gradient(135deg, #1f2937, #374151)';
const placeholder = document.createElement('div');
placeholder.className = 'thumbnail-placeholder absolute inset-0 flex flex-col items-center justify-center text-white';
placeholder.innerHTML = '<div style="font-size: 28px; margin-bottom: 4px;">🎬</div><div style="font-size: 10px; opacity: 0.7;">Video</div>';
target.parentElement.appendChild(placeholder);
}
}}
/>
{/* Duration badge */}
<div className="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded">
{formatDuration(video.duration)}
</div>
{/* Play button overlay */}
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-all duration-300 flex items-center justify-center">
<div className="bg-white/20 backdrop-blur-sm rounded-full w-16 h-16 flex items-center justify-center group-hover:bg-white/30 group-hover:scale-110 transition-all duration-300">
<div className="w-0 h-0 border-l-[12px] border-l-white border-y-[8px] border-y-transparent ml-1"></div>
</div>
</div>
</div>
<div className="space-y-2">
<h3
className="font-semibold line-clamp-2 hover:text-bunny-blue transition-colors text-bunny-light cursor-pointer"
onClick={() => onClick?.(video)}
data-testid={`text-title-${video.id}`}
>
{video.title}
</h3>
<div className="flex items-center space-x-3 text-sm text-bunny-muted">
<span data-testid={`text-views-${video.id}`}>
{formatViews(video.views)}
</span>
<span data-testid={`text-date-${video.id}`}>
{formatDate(video.createdAt)}
</span>
</div>
</div>
</div>
);
}