Files
usda-vision/src/features/video-streaming/components/VideoCard.tsx
Alireza Vaezi 81828f61cf feat(video-streaming): add ApiStatusIndicator, PerformanceDashboard, VideoDebugger, and VideoErrorBoundary components
- Implemented ApiStatusIndicator to monitor video API connection status with health check functionality.
- Created PerformanceDashboard for monitoring video streaming performance metrics in development mode.
- Developed VideoDebugger for diagnosing video streaming issues with direct access to test video URLs.
- Added VideoErrorBoundary to handle errors in video streaming components with user-friendly messages and recovery options.
- Introduced utility functions for performance monitoring and thumbnail caching to optimize video streaming operations.
- Added comprehensive tests for video streaming API connectivity and functionality.
2025-08-06 11:46:25 -04:00

172 lines
6.5 KiB
TypeScript

/**
* VideoCard Component
*
* A reusable card component for displaying video information with thumbnail, metadata, and actions.
*/
import React from 'react';
import { type VideoCardProps } from '../types';
import { VideoThumbnail } from './VideoThumbnail';
import {
formatFileSize,
formatVideoDate,
getRelativeTime,
getFormatDisplayName,
getStatusBadgeClass,
getResolutionString,
} from '../utils/videoUtils';
export const VideoCard: React.FC<VideoCardProps> = ({
video,
onClick,
showMetadata = true,
className = '',
}) => {
const handleClick = () => {
if (onClick) {
onClick(video);
}
};
const handleThumbnailClick = () => {
handleClick();
};
const cardClasses = [
'bg-white rounded-xl border border-gray-200 overflow-hidden transition-all hover:shadow-theme-md',
onClick ? 'cursor-pointer hover:border-gray-300' : '',
className,
].filter(Boolean).join(' ');
return (
<div className={cardClasses} onClick={onClick ? handleClick : undefined}>
{/* Thumbnail */}
<div className="relative">
<VideoThumbnail
fileId={video.file_id}
width={320}
height={180}
alt={`Thumbnail for ${video.filename}`}
onClick={onClick ? handleThumbnailClick : undefined}
className="w-full"
/>
{/* Status Badge */}
<div className="absolute top-2 left-2">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusBadgeClass(video.status)}`}>
{video.status}
</span>
</div>
{/* Format Badge */}
<div className="absolute top-2 right-2">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{getFormatDisplayName(video.format)}
</span>
</div>
{/* Streamable Indicator */}
{video.is_streamable ? (
<div className="absolute bottom-2 left-2">
<div className="bg-green-500 text-white text-xs px-2 py-1 rounded flex items-center">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clipRule="evenodd" />
</svg>
Streamable
</div>
</div>
) : (
<div className="absolute bottom-2 left-2">
<div className="bg-yellow-500 text-white text-xs px-2 py-1 rounded flex items-center">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
Processing
</div>
</div>
)}
{/* Conversion Needed Indicator */}
{video.needs_conversion && (
<div className="absolute bottom-2 right-2">
<div className="bg-yellow-500 text-white text-xs px-2 py-1 rounded flex items-center">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
Needs Conversion
</div>
</div>
)}
</div>
{/* Content */}
<div className="p-4">
{/* Title */}
<h3 className="text-lg font-semibold text-gray-900 mb-2 truncate" title={video.filename}>
{video.filename}
</h3>
{/* Camera Name */}
<div className="flex items-center text-sm text-gray-600 mb-2">
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>
{video.camera_name}
</div>
{/* Basic Info */}
<div className="grid grid-cols-2 gap-4 text-sm text-gray-600 mb-3">
<div>
<span className="font-medium">Size:</span> {formatFileSize(video.file_size_bytes)}
</div>
<div>
<span className="font-medium">Created:</span> {getRelativeTime(video.created_at)}
</div>
</div>
{/* Metadata (if available and requested) */}
{showMetadata && 'metadata' in video && video.metadata && (
<div className="border-t pt-3 mt-3 border-gray-100">
<div className="grid grid-cols-2 gap-4 text-sm text-gray-600">
<div>
<span className="font-medium">Duration:</span> {Math.round(video.metadata.duration_seconds)}s
</div>
<div>
<span className="font-medium">Resolution:</span> {getResolutionString(video.metadata.width, video.metadata.height)}
</div>
<div>
<span className="font-medium">FPS:</span> {video.metadata.fps}
</div>
<div>
<span className="font-medium">Codec:</span> {video.metadata.codec}
</div>
</div>
</div>
)}
{/* Actions */}
<div className="flex justify-between items-center mt-4 pt-3 border-t border-gray-100">
<div className="text-xs text-gray-500">
{formatVideoDate(video.created_at)}
</div>
{onClick && (
<button
onClick={(e) => {
e.stopPropagation();
handleClick();
}}
className="inline-flex items-center px-3 py-1.5 text-xs font-medium transition rounded-lg border border-transparent bg-brand-500 text-white hover:bg-brand-600 shadow-theme-xs"
>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clipRule="evenodd" />
</svg>
Play
</button>
)}
</div>
</div>
</div>
);
};