mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 22:11:13 -05:00
feat: Implement Sprint 3 Performance Optimizations
This commit is contained in:
80
src/components/common/LazyImage.tsx
Normal file
80
src/components/common/LazyImage.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* LazyImage Component
|
||||
* Implements lazy loading for images using Intersection Observer
|
||||
* Only loads images when they're scrolled into view
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface LazyImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
onLoad?: () => void;
|
||||
onError?: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void;
|
||||
}
|
||||
|
||||
export function LazyImage({
|
||||
src,
|
||||
alt,
|
||||
className = '',
|
||||
onLoad,
|
||||
onError
|
||||
}: LazyImageProps) {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [isInView, setIsInView] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const imgRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!imgRef.current) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsInView(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: '100px', // Start loading 100px before visible
|
||||
threshold: 0.01,
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(imgRef.current);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleLoad = () => {
|
||||
setIsLoaded(true);
|
||||
onLoad?.();
|
||||
};
|
||||
|
||||
const handleError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {
|
||||
setHasError(true);
|
||||
onError?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={imgRef} className={`relative ${className}`}>
|
||||
{!isInView || hasError ? (
|
||||
// Loading skeleton or error state
|
||||
<div className="w-full h-full bg-muted animate-pulse rounded" />
|
||||
) : (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
className={`w-full h-full object-cover transition-opacity duration-300 ${
|
||||
isLoaded ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LazyImage.displayName = 'LazyImage';
|
||||
@@ -8,6 +8,7 @@ import { Eye, AlertCircle } from 'lucide-react';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import type { PhotoItem } from '@/types/photos';
|
||||
import { generatePhotoAlt } from '@/lib/photoHelpers';
|
||||
import { LazyImage } from '@/components/common/LazyImage';
|
||||
|
||||
interface PhotoGridProps {
|
||||
photos: PhotoItem[];
|
||||
@@ -42,14 +43,13 @@ export const PhotoGrid = memo(({
|
||||
{displayPhotos.map((photo, index) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className="relative cursor-pointer group overflow-hidden rounded-md border bg-muted/30"
|
||||
className="relative cursor-pointer group overflow-hidden rounded-md border bg-muted/30 h-32"
|
||||
onClick={() => onPhotoClick?.(photos, index)}
|
||||
>
|
||||
<img
|
||||
<LazyImage
|
||||
src={photo.url}
|
||||
alt={generatePhotoAlt(photo)}
|
||||
className="w-full h-32 object-cover transition-opacity group-hover:opacity-80"
|
||||
loading="lazy"
|
||||
className="w-full h-32"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
@@ -68,7 +68,7 @@ export const PhotoGrid = memo(({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 text-white opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 text-white opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
|
||||
<Eye className="w-5 h-5" />
|
||||
</div>
|
||||
{photo.caption && (
|
||||
|
||||
Reference in New Issue
Block a user