All files / src/lib/image index.ts

65.49% Statements 93/142
50% Branches 1/2
20% Functions 1/5
65.49% Lines 93/142

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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 1431x 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 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 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  
/**
 * Image Utilities
 *
 * Centralized image configuration and helpers for the application.
 * Includes Cloudinary integration and responsive image utilities.
 */
 
import { logger } from '@/lib/logging';
 
/**
 * Get the Cloudinary cloud name from environment
 */
export function getCloudinaryCloudName(): string {
  return process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME || '';
}
 
/**
 * Cloudinary transformation options
 */
export interface CloudinaryTransformOptions {
  /** Width in pixels */
  width?: number;
  /** Height in pixels */
  height?: number;
  /** Quality (auto, 1-100) */
  quality?: 'auto' | number;
  /** Format (auto, webp, avif, jpg, png) */
  format?: 'auto' | 'webp' | 'avif' | 'jpg' | 'png';
  /** Crop mode */
  crop?: 'fill' | 'fit' | 'scale' | 'thumb';
  /** Gravity for cropping */
  gravity?: 'auto' | 'center' | 'face' | 'faces';
  /** Blur amount (1-2000) */
  blur?: number;
}
 
/**
 * Build a Cloudinary URL with transformations
 */
export function buildCloudinaryUrl(
  publicId: string,
  options: CloudinaryTransformOptions = {}
): string {
  const cloudName = getCloudinaryCloudName();

  if (!cloudName) {
    logger.warn('Cloudinary cloud name not configured', {
      category: 'SYSTEM',
    });
    return '';
  }

  const transforms: string[] = [];

  if (options.width) transforms.push(`w_${options.width}`);
  if (options.height) transforms.push(`h_${options.height}`);
  if (options.crop) transforms.push(`c_${options.crop}`);
  if (options.gravity) transforms.push(`g_${options.gravity}`);
  if (options.blur) transforms.push(`e_blur:${options.blur}`);

  transforms.push(`q_${options.quality || 'auto'}`);
  transforms.push(`f_${options.format || 'auto'}`);

  const transformString = transforms.join(',');
  return `https://res.cloudinary.com/${cloudName}/image/upload/${transformString}/${publicId}`;
}
 
/**
 * Generate a blur placeholder URL for Cloudinary images
 */
export function buildCloudinaryBlurPlaceholder(publicId: string): string {
  return buildCloudinaryUrl(publicId, {
    width: 20,
    quality: 30,
    blur: 1000,
  });
}
 
/**
 * Generate srcset for responsive images
 */
export function buildCloudinarySrcset(
  publicId: string,
  widths: number[] = [320, 640, 768, 1024, 1280, 1536],
  format?: 'webp' | 'avif'
): string {
  return widths
    .map((width) => {
      const url = buildCloudinaryUrl(publicId, { width, format });
      return `${url} ${width}w`;
    })
    .join(', ');
}
 
/**
 * Generate a blur placeholder SVG data URL
 */
export function generateBlurPlaceholder(
  width: number,
  height: number,
  color = '#e5e7eb'
): string {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">
    <rect fill="${color}" width="${width}" height="${height}"/>
  </svg>`;
 
  // Use btoa for browser, Buffer for server
  const base64 =
    typeof window !== 'undefined'
      ? btoa(svg)
      : Buffer.from(svg).toString('base64');
 
  return `data:image/svg+xml;base64,${base64}`;
}
 
/**
 * Default placeholder image path
 */
export const DEFAULT_PLACEHOLDER = '/images/placeholder.png';
 
/**
 * Aspect ratio CSS classes
 */
export const aspectRatioClasses: Record<string, string> = {
  '1:1': 'aspect-square',
  '4:3': 'aspect-[4/3]',
  '16:9': 'aspect-video',
  '3:4': 'aspect-[3/4]',
  '21:9': 'aspect-[21/9]',
  auto: '',
};
 
/**
 * Object fit CSS classes
 */
export const objectFitClasses: Record<string, string> = {
  cover: 'object-cover',
  contain: 'object-contain',
  fill: 'object-fill',
  none: 'object-none',
  'scale-down': 'object-scale-down',
};