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