All files / src/components/ui/ProductImage index.tsx

92.18% Statements 118/128
71.42% Branches 5/7
40% Functions 2/5
92.18% Lines 118/128

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 1291x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x   258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 258x 1x 1x 1x 1x 1x 1x 1x 1x 1x 46x 46x 46x 46x 46x 46x 46x 46x 46x 1x 1x 1x 1x 1x 1x 1x 1x 1x                    
'use client';
 
import { useState } from 'react';
import Image, { ImageProps } from 'next/image';
import { cn } from '@/lib/core';
 
/**
 * ProductImage Component Props
 */
export interface ProductImageProps extends Omit<ImageProps, 'src' | 'onError'> {
  /** Image source URL */
  src: string | null | undefined;
  /** Fallback image URL */
  fallback?: string;
  /** Show loading skeleton */
  showSkeleton?: boolean;
  /** Container class name */
  containerClassName?: string;
}
 
/**
 * ProductImage - Enhanced Next.js Image with built-in error handling
 *
 * Features:
 * - Automatic fallback to placeholder on error
 * - Loading skeleton support
 * - Handles null/undefined src gracefully
 * - Optimized for product images
 * - Consistent placeholder across app
 *
 * @example
 * ```tsx
 * // Basic usage
 * <ProductImage
 *   src={product.imgs?.previews[0]}
 *   alt={product.title}
 *   width={250}
 *   height={250}
 * />
 *
 * // With custom fallback
 * <ProductImage
 *   src={imageSrc}
 *   fallback="/images/custom-placeholder.png"
 *   alt="Product"
 *   width={200}
 *   height={200}
 * />
 *
 * // With loading skeleton
 * <ProductImage
 *   src={product.image}
 *   showSkeleton
 *   alt="Product"
 *   width={300}
 *   height={300}
 * />
 * ```
 */
export function ProductImage({
  src,
  fallback = '/images/placeholder.png',
  showSkeleton = false,
  containerClassName,
  className,
  alt,
  ...props
}: ProductImageProps) {
  const [imageError, setImageError] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
 
  // Use fallback if src is null/undefined or if image failed to load
  const imageSrc = !src || imageError ? fallback : src;
 
  return (
    <div className={cn('relative', containerClassName)}>
      {showSkeleton && isLoading && (
        <div className="absolute inset-0 animate-pulse bg-gray-200 dark:bg-gray-700 rounded" />
      )}
 
      <Image
        src={imageSrc}
        alt={alt}
        className={cn(className)}
        onError={() => setImageError(true)}
        onLoad={() => setIsLoading(false)}
        placeholder="empty"
        {...props}
      />
    </div>
  );
}
 
/**
 * ProductThumbnail - Optimized for small product thumbnails
 */
export interface ProductThumbnailProps extends Omit<ProductImageProps, 'width' | 'height'> {
  /** Thumbnail size (default: 90) */
  size?: number;
}
 
export function ProductThumbnail({ size = 90, ...props }: ProductThumbnailProps) {
  return (
    <ProductImage
      width={size}
      height={size}
      {...props}
    />
  );
}
 
/**
 * ProductPreview - Optimized for product preview images
 */
export interface ProductPreviewProps extends Omit<ProductImageProps, 'width' | 'height'> {
  /** Preview size (default: 250) */
  size?: number;
}
 
export function ProductPreview({ size = 250, ...props }: ProductPreviewProps) {
  return (
    <ProductImage
      width={size}
      height={size}
      {...props}
    />
  );
}