useResponsiveImage.ts raw

   1  /**
   2   * Hook for responsive image variant selection
   3   *
   4   * Automatically fetches and selects the appropriate image variant
   5   * based on container width and device pixel ratio.
   6   */
   7  
   8  import { UploadedVariant } from '@/lib/responsive-image-event'
   9  import responsiveImageService from '@/services/responsive-image.service'
  10  import { TImetaInfo } from '@/types'
  11  import { useEffect, useMemo, useState } from 'react'
  12  
  13  export type UseResponsiveImageOptions = {
  14    /** Target container width (defaults to viewport width) */
  15    containerWidth?: number
  16    /** Force use of original variant (for lightbox) */
  17    useOriginal?: boolean
  18    /** Force use of thumbnail variant */
  19    useThumbnail?: boolean
  20  }
  21  
  22  export type UseResponsiveImageResult = {
  23    /** The selected image info (may be original or a variant) */
  24    imageInfo: TImetaInfo
  25    /** All available variants (empty if not a responsive image) */
  26    variants: UploadedVariant[]
  27    /** Whether variants are still loading */
  28    isLoading: boolean
  29    /** The original/full size variant URL (for lightbox) */
  30    originalUrl: string
  31    /** Whether this image has responsive variants */
  32    hasVariants: boolean
  33  }
  34  
  35  /**
  36   * Hook to get responsive image variant for display
  37   *
  38   * @param image - The original image info
  39   * @param options - Selection options
  40   */
  41  export function useResponsiveImage(
  42    image: TImetaInfo,
  43    options: UseResponsiveImageOptions = {}
  44  ): UseResponsiveImageResult {
  45    const { containerWidth, useOriginal = false, useThumbnail = false } = options
  46  
  47    const [variants, setVariants] = useState<UploadedVariant[]>([])
  48    const [isLoading, setIsLoading] = useState(false)
  49  
  50    // Get sha256 from image (from imeta tag or extract from URL)
  51    const sha256 = useMemo(() => {
  52      if (image.sha256) return image.sha256
  53      return responsiveImageService.extractSha256FromUrl(image.url)
  54    }, [image.sha256, image.url])
  55  
  56    // Fetch variants when sha256 is available
  57    useEffect(() => {
  58      if (!sha256) {
  59        setVariants([])
  60        return
  61      }
  62  
  63      let cancelled = false
  64      setIsLoading(true)
  65  
  66      responsiveImageService.getVariantsForHash(sha256).then((result) => {
  67        if (cancelled) return
  68        setVariants(result ?? [])
  69        setIsLoading(false)
  70      })
  71  
  72      return () => {
  73        cancelled = true
  74      }
  75    }, [sha256])
  76  
  77    // Get viewport width for responsive selection
  78    const viewportWidth = useMemo(() => {
  79      if (typeof window === 'undefined') return 1280
  80      return window.innerWidth
  81    }, [])
  82  
  83    // Select the best variant
  84    const selectedVariant = useMemo(() => {
  85      if (variants.length === 0) return null
  86  
  87      if (useOriginal) {
  88        return responsiveImageService.getOriginalVariant(variants)
  89      }
  90  
  91      if (useThumbnail) {
  92        return responsiveImageService.getThumbnailVariant(variants)
  93      }
  94  
  95      const targetWidth = containerWidth ?? viewportWidth
  96      const pixelRatio = typeof window !== 'undefined' ? window.devicePixelRatio : 1
  97  
  98      return responsiveImageService.selectVariant(variants, targetWidth, pixelRatio)
  99    }, [variants, containerWidth, viewportWidth, useOriginal, useThumbnail])
 100  
 101    // Build the result image info
 102    const imageInfo: TImetaInfo = useMemo(() => {
 103      if (!selectedVariant) return image
 104  
 105      return {
 106        url: selectedVariant.url,
 107        sha256: selectedVariant.sha256,
 108        blurHash: selectedVariant.blurhash ?? image.blurHash,
 109        thumbHash: image.thumbHash,
 110        dim: {
 111          width: selectedVariant.width,
 112          height: selectedVariant.height
 113        },
 114        pubkey: image.pubkey,
 115        variant: selectedVariant.variant
 116      }
 117    }, [selectedVariant, image])
 118  
 119    // Get original URL for lightbox
 120    const originalUrl = useMemo(() => {
 121      if (variants.length === 0) return image.url
 122      const original = responsiveImageService.getOriginalVariant(variants)
 123      return original?.url ?? image.url
 124    }, [variants, image.url])
 125  
 126    return {
 127      imageInfo,
 128      variants,
 129      isLoading,
 130      originalUrl,
 131      hasVariants: variants.length > 0
 132    }
 133  }
 134  
 135  export default useResponsiveImage
 136