banatie-service/apps/landing/src/components/demo/gallery/GalleryImageCard.tsx

175 lines
5.7 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useIntersectionObserver } from '@/hooks/useIntersectionObserver';
import { useImageDownloadTime } from '@/components/shared/ImageCard/useImageDownloadTime';
import { ImageMetadataBar } from '@/components/shared/ImageMetadataBar';
import { calculateAspectRatio } from '@/utils/imageUtils';
import { usePageContext } from '@/contexts/page-context';
import { ExpandedImageView } from '@/components/shared/ExpandedImageView';
type GalleryImageCardProps = {
imageUrl: string;
filename: string;
size: number;
contentType: string;
lastModified: string;
onDownloadMeasured: (imageId: string, downloadMs: number) => void;
};
export const GalleryImageCard = ({
imageUrl,
filename,
size,
contentType,
lastModified,
onDownloadMeasured,
}: GalleryImageCardProps) => {
const { openModal } = usePageContext();
const [isVisible, setIsVisible] = useState(false);
const [imageDimensions, setImageDimensions] = useState<{
width: number;
height: number;
} | null>(null);
const { ref } = useIntersectionObserver({
onIntersect: () => setIsVisible(true),
threshold: 0.1,
});
const { downloadTime } = useImageDownloadTime(isVisible ? imageUrl : null);
useEffect(() => {
if (downloadTime !== null) {
onDownloadMeasured(imageUrl, downloadTime);
}
}, [downloadTime, imageUrl, onDownloadMeasured]);
useEffect(() => {
if (!isVisible) return;
const img = new Image();
img.onload = () => {
setImageDimensions({
width: img.width,
height: img.height,
});
};
img.src = imageUrl;
}, [isVisible, imageUrl]);
const handleImageClick = () => {
openModal(<ExpandedImageView imageUrl={imageUrl} alt={filename} />);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleImageClick();
}
};
const formatTimestamp = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<div
ref={ref}
className="p-4 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-xl hover:border-slate-600 transition-all h-full"
>
<div
className="aspect-video bg-slate-800 rounded-lg mb-3 overflow-hidden cursor-pointer group relative focus:outline-none focus:ring-2 focus:ring-amber-500"
onClick={handleImageClick}
role="button"
tabIndex={0}
aria-label={`View full size image: ${filename}`}
onKeyDown={handleKeyDown}
>
{isVisible ? (
<>
<img
src={imageUrl}
alt={filename}
className="w-full h-full object-cover transition-transform group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-transparent to-black/70 opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity">
<div className="absolute top-3 left-3">
<span className="px-3 py-2 bg-black/50 backdrop-blur-sm text-white text-xs sm:text-sm font-medium rounded border border-white/10" aria-label={`Created ${formatTimestamp(lastModified)}`}>
{formatTimestamp(lastModified)}
</span>
</div>
<div className="absolute top-3 right-3 max-w-[60%]">
<span
className="px-3 py-2 bg-black/50 backdrop-blur-sm text-white text-xs sm:text-sm font-medium rounded border border-white/10 truncate block"
title={filename}
aria-label={`Filename: ${filename}`}
>
{filename}
</span>
</div>
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="bg-black/60 backdrop-blur-sm rounded-full p-4">
<svg
className="w-10 h-10 sm:w-12 sm:h-12 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"
/>
</svg>
</div>
</div>
</div>
</>
) : (
<div className="w-full h-full flex items-center justify-center bg-slate-800 animate-pulse" aria-label="Loading image">
<div className="text-gray-600">
<svg
className="w-12 h-12"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
</div>
)}
</div>
{imageDimensions && (
<ImageMetadataBar
width={imageDimensions.width}
height={imageDimensions.height}
fileSize={size}
fileType={contentType}
showVisualIndicator={true}
downloadMs={downloadTime}
/>
)}
</div>
);
};