Skip to content

Instantly share code, notes, and snippets.

@revnelson
Last active July 19, 2024 14:59
Show Gist options
  • Save revnelson/046b87cfb981be0909bc9e475efacdef to your computer and use it in GitHub Desktop.
Save revnelson/046b87cfb981be0909bc9e475efacdef to your computer and use it in GitHub Desktop.
"use client"
import { memo, useCallback, useEffect, useRef, useState } from "react"
import { getImageProps } from "next/image"
import type {
CMSHeroLargeImage,
CMSHeroMediumImage,
CMSHeroSmallImage
} from "@/lib/api/types"
import type { ImageProps } from "next/image"
import { BlurhashCanvas } from "react-blurhash"
import { cn } from "@/lib/utils"
// TODO - Implement ResponsiveImage group in backend
type CMSImage = Omit<
CMSHeroSmallImage | CMSHeroMediumImage | CMSHeroLargeImage,
"__typename"
>
type Props = {
images: {
small?: CMSImage | null
medium?: CMSImage | null
large?: CMSImage | null
}
imgClasses?: string
} & Partial<ImageProps>
const ResponsiveImage = ({
className,
fill,
priority,
imgClasses,
sizes = "100vw",
style = { objectFit: "cover" },
images,
...props
}: Props) => {
const commonPreload = {
rel: "preload",
as: "image",
imageSizes: sizes
}
const { small, medium, large } = images || {}
const alt = large?.alt || medium?.alt || small?.alt || ""
const width = large?.width || medium?.width || small?.width || 0
const height = large?.height || medium?.height || small?.height || 0
const common = { alt, fill, priority, sizes, style, width, height, ...props }
const quality = 50
const { srcSet: smallSet, ...smallProps } = getImageProps({
...common,
src: small?.url || "",
quality
}).props
const { srcSet: mediumSet, ...mediumProps } = getImageProps({
...common,
src: medium?.url || "",
quality
}).props
const { srcSet: largeSet, ...largeProps } = getImageProps({
...common,
src: large?.url || "",
quality
}).props
const smallMedia =
medium || large
? `(max-width: ${medium ? 767 : 1023}px)`
: "(min-width: 0px)"
const mediumMedia = `${small ? `(min-width: 768px)${large ? " and " : ""}` : ""} ${
large ? `(max-width: 1023px)` : ""
}`
const largeMedia = `(min-width: ${medium || small ? 1024 : 0}px)`
const blurhash = large?.blurhash || medium?.blurhash || small?.blurhash
const container = useRef<HTMLDivElement>(null)
const [imgLoaded, setImgLoaded] = useState(false)
const [clientWidth, setClientWidth] = useState(0)
const [intersecting, setIntersecting] = useState(false)
const [native, setNative] = useState(false)
// const [hasResizeObserver, setHasResizeObserver] = useState(true)
const [hidePlaceholder, setHidePlaceholder] = useState(false)
// const [mounted, setMounted] = useState(false)
const [imageWidth, setImageWidth] = useState(0)
// const [sizes, setSizes] = useState(
// imageWidth ? `${imageWidth}px` : undefined
// )
const imgRef = useRef<HTMLImageElement>(null)
const blurRef = useRef<BlurhashCanvas>(null)
const handleOnLoad = useCallback(
(from: string = "") => {
setImgLoaded(true)
if (!priority) {
setTimeout(() => {
setHidePlaceholder(true)
}, 250) // sync with opacity transition duration
}
},
[priority]
)
useEffect(() => {
const currentContainer = container.current
// setMounted(true)
let ro: ResizeObserver
if (window.ResizeObserver) {
ro = new ResizeObserver((entries) => {
setClientWidth(entries[0].contentRect.width)
setImageWidth(
width && clientWidth
? Math.min(clientWidth, Number(width))
: width ?? clientWidth
)
// setSizes(imageWidth ? `${imageWidth}px` : undefined)
})
currentContainer && ro.observe(currentContainer)
} else {
// setHasResizeObserver(false)
}
setNative("loading" in HTMLImageElement.prototype)
if (native || priority) {
return () => {
if (ro && currentContainer) {
ro.unobserve(currentContainer)
}
}
}
const io = new IntersectionObserver(
(entries) => {
setIntersecting(entries[0].isIntersecting)
if (intersecting && currentContainer) {
io.unobserve(currentContainer)
}
},
{
rootMargin: `100px`
}
)
currentContainer && io.observe(currentContainer)
return () => {
currentContainer && io.unobserve(currentContainer)
if (ro && currentContainer) {
ro.unobserve(currentContainer)
}
}
}, [clientWidth, imageWidth, priority, intersecting, native, width])
useEffect(() => {
imgRef.current?.complete && setImgLoaded(true)
}, [imgRef.current?.complete])
useEffect(() => {
if (hidePlaceholder && blurRef.current) {
blurRef.current.canvas.hidden = true
}
}, [hidePlaceholder])
return (
<div ref={container} className={cn("relative w-full h-full", className)}>
{priority && (
<>
{large?.url && (
<link
{...commonPreload}
media={largeMedia}
href={large?.url || ""}
imageSrcSet={largeSet}
/>
)}
{medium?.url && (
<link
{...commonPreload}
media={mediumMedia}
href={medium.url || ""}
imageSrcSet={mediumSet}
/>
)}
{small?.url && (
<link
{...commonPreload}
media={smallMedia}
href={small?.url || ""}
imageSrcSet={smallSet}
/>
)}
</>
)}
<picture>
{large && <source media={largeMedia} srcSet={largeSet} />}
{medium && <source media={mediumMedia} srcSet={mediumSet} />}
{small && <source media={smallMedia} srcSet={smallSet} />}
<img
alt={alt}
ref={imgRef}
{...(largeProps.src
? largeProps
: mediumProps.src
? mediumProps
: smallProps)}
onLoad={() => handleOnLoad("picture")}
className={` ${
imgLoaded || priority ? "loaded" : ""
} w-full h-full object-cover "${imgClasses ? ` ${imgClasses}` : ""}`}
style={{ objectPosition: "center" }}
/>
</picture>
{blurhash?.hash && blurhash.width && blurhash.height && (
<BlurhashCanvas
hash={blurhash.hash}
width={blurhash.width}
height={blurhash.height}
ref={blurRef}
punch={1}
className={`absolute bottom-0 w-full h-full object-cover trans duration-300 ${
imgLoaded ? "opacity-0" : ""
}`}
/>
)}
</div>
)
}
export default ResponsiveImage
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment