/** * ImageWithSkeleton Component * * Progressive image loading with skeleton placeholder: * - Shows skeleton while image loads * - Smooth fade transition on load * - Supports lazy loading (loads last in priority) * - Error state handling */ import { useState, useCallback } from 'react'; import styled from 'styled-components'; import { Skeleton } from './Skeleton'; export interface ImageWithSkeletonProps { /** Image source URL */ src: string; /** Alt text for accessibility */ alt: string; /** Width (default: 100%) */ width?: string | number; /** Height (default: auto) */ height?: string | number; /** Border radius */ borderRadius?: string | number; /** Object fit style */ objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; /** Lazy loading (default: true) */ lazy?: boolean; /** Custom className */ className?: string; /** Fallback image on error */ fallbackSrc?: string; /** Aspect ratio (e.g., "16/9", "1/1") */ aspectRatio?: string; /** Loading priority (for fetch priority API) */ priority?: 'high' | 'low' | 'auto'; /** Callback when image loads */ onLoad?: () => void; /** Callback when image fails to load */ onError?: () => void; } const ImageContainer = styled.div<{ $width?: string | number; $height?: string | number; $aspectRatio?: string; $borderRadius?: string | number; }>` position: relative; width: ${({ $width }) => $width ? (typeof $width === 'number' ? `${$width}px` : $width) : '100%'}; height: ${({ $height }) => $height ? (typeof $height === 'number' ? `${$height}px` : $height) : 'auto'}; aspect-ratio: ${({ $aspectRatio }) => $aspectRatio || 'auto'}; border-radius: ${({ $borderRadius }) => $borderRadius ? typeof $borderRadius === 'number' ? `${$borderRadius}px` : $borderRadius : '0'}; overflow: hidden; `; const StyledImage = styled.img<{ $loaded: boolean; $objectFit?: ImageWithSkeletonProps['objectFit']; }>` width: 100%; height: 100%; object-fit: ${({ $objectFit }) => $objectFit || 'cover'}; opacity: ${({ $loaded }) => ($loaded ? 1 : 0)}; transition: opacity 0.3s ease-in-out; `; const SkeletonOverlay = styled.div<{ $visible: boolean }>` position: absolute; top: 0; left: 0; right: 0; bottom: 0; opacity: ${({ $visible }) => ($visible ? 1 : 0)}; transition: opacity 0.3s ease-in-out; pointer-events: none; `; const ErrorPlaceholder = styled.div` display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; min-height: 100px; background: rgba(255, 255, 255, 0.05); color: rgba(255, 255, 255, 0.5); font-size: 14px; `; export function ImageWithSkeleton({ src, alt, width, height, borderRadius, objectFit = 'cover', lazy = true, className, fallbackSrc, aspectRatio, priority = 'low', onLoad, onError, }: ImageWithSkeletonProps) { const [loaded, setLoaded] = useState(false); const [error, setError] = useState(false); const [currentSrc, setCurrentSrc] = useState(src); const handleLoad = useCallback(() => { setLoaded(true); onLoad?.(); }, [onLoad]); const handleError = useCallback(() => { if (fallbackSrc && currentSrc !== fallbackSrc) { setCurrentSrc(fallbackSrc); } else { setError(true); onError?.(); } }, [fallbackSrc, currentSrc, onError]); if (error) { return ( Failed to load image ); } return ( ); } /** * Avatar image with skeleton */ export function AvatarWithSkeleton({ src, alt, size = 40, className, fallbackSrc, }: { src: string; alt: string; size?: number; className?: string; fallbackSrc?: string; }) { return ( ); } /** * Hero image with skeleton (higher priority) */ export function HeroImageWithSkeleton({ src, alt, aspectRatio = '16/9', className, }: { src: string; alt: string; aspectRatio?: string; className?: string; }) { return ( ); }