feat: Implement Sprint 3 Performance Optimizations

This commit is contained in:
gpt-engineer-app[bot]
2025-11-02 21:52:59 +00:00
parent a9644c0bee
commit d057ddc8cc
7 changed files with 746 additions and 97 deletions

View 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';

View File

@@ -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 && (