-
Notifications
You must be signed in to change notification settings - Fork 65
Expand file tree
/
Copy pathImage.tsx
More file actions
171 lines (150 loc) · 4.44 KB
/
Image.tsx
File metadata and controls
171 lines (150 loc) · 4.44 KB
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import type { ImgHTMLAttributes } from 'react'
export interface ImageProps extends ImgHTMLAttributes<HTMLImageElement> {
src: string
alt: string
width?: number | string
height?: number | string
fill?: boolean
priority?: boolean
quality?: number
placeholder?: 'blur' | 'empty'
blurDataURL?: string
loader?: (params: { src: string; width: number; quality?: number }) => string
unoptimized?: boolean
}
import type { CSSProperties, ReactElement } from 'react'
import { useEffect, useRef, useState } from 'react'
export interface CustomImageProps extends ImageProps {
fallbackSrc?: string
onLoadStart?: () => void
onLoadComplete?: () => void
onError?: () => void
}
function Image(props: CustomImageProps): ReactElement {
const {
src,
alt = '',
width,
height,
fallbackSrc = '/placeholder.png',
onLoadComplete,
onError,
loading = 'lazy',
className = '',
style,
fill,
priority,
...rest
} = props
const [imageSrc, setImageSrc] = useState<string | typeof src>(src)
const [prevSrc, setPrevSrc] = useState(src)
const [isVisible, setIsVisible] = useState(loading !== 'lazy' || priority === true)
const [isLoading, setIsLoading] = useState(true)
const [hasError, setHasError] = useState(false)
const imageRef = useRef<HTMLDivElement>(null)
const imgRef = useRef<HTMLImageElement>(null)
const observerRef = useRef<IntersectionObserver | null>(null)
// Render-time state adjustment: reset when src prop changes
if (src !== prevSrc) {
setPrevSrc(src)
setImageSrc(src)
setHasError(false)
setIsLoading(true)
}
// Set up IntersectionObserver for lazy loading
useEffect(() => {
if (loading !== 'lazy' || priority || !imageRef.current) return
const observerOptions: IntersectionObserverInit = {
root: null,
rootMargin: '50px',
threshold: 0.01
}
observerRef.current = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true)
observerRef.current?.disconnect()
}
})
}, observerOptions)
observerRef.current.observe(imageRef.current)
return () => {
observerRef.current?.disconnect()
}
}, [loading, priority])
// Handle already-cached images where onLoad might not fire
useEffect(() => {
if (!isVisible) return
const imageElement = imgRef.current
if (imageElement?.complete && imageElement.naturalWidth > 0 && imageElement.naturalHeight > 0) {
setIsLoading(false)
setHasError(false)
}
}, [isVisible])
const handleLoadComplete = (): void => {
setIsLoading(false)
setHasError(false)
onLoadComplete?.()
}
const handleError = (): void => {
setHasError(true)
setIsLoading(false)
// Try fallback if available and not already using it
if (fallbackSrc && imageSrc !== fallbackSrc) {
setImageSrc(fallbackSrc)
}
onError?.()
}
const containerStyle: CSSProperties = {
position: fill ? 'relative' : 'static',
display: fill ? 'block' : 'inline-block',
width: fill ? '100%' : undefined,
height: fill ? '100%' : undefined,
...style
}
const imageClassName = [
className,
isLoading && !hasError ? 'opacity-0' : 'opacity-100',
'transition-opacity duration-300 ease-in-out'
]
.filter(Boolean)
.join(' ')
// Error state
const errorPlaceholder = hasError && imageSrc === fallbackSrc && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded">
<span className="text-gray-400 dark:text-gray-500 text-sm">Failed to load image</span>
</div>
)
return (
<div ref={imageRef} style={containerStyle} className="relative overflow-hidden">
{errorPlaceholder}
{isVisible && (
<img
ref={imgRef}
src={imageSrc as string}
alt={alt}
className={imageClassName}
onLoad={handleLoadComplete}
onError={handleError}
width={!fill ? width : undefined}
height={!fill ? height : undefined}
style={
fill
? {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover'
}
: undefined
}
decoding="async"
{...rest}
/>
)}
</div>
)
}
export default Image