|
|
|
@ -2,20 +2,29 @@ import { Skeleton } from '@/components/ui/skeleton' |
|
|
|
import { cn } from '@/lib/utils' |
|
|
|
import { cn } from '@/lib/utils' |
|
|
|
import { TImageInfo } from '@/types' |
|
|
|
import { TImageInfo } from '@/types' |
|
|
|
import { decode } from 'blurhash' |
|
|
|
import { decode } from 'blurhash' |
|
|
|
|
|
|
|
import { ImageOff } from 'lucide-react' |
|
|
|
import { HTMLAttributes, useEffect, useState } from 'react' |
|
|
|
import { HTMLAttributes, useEffect, useState } from 'react' |
|
|
|
|
|
|
|
|
|
|
|
export default function Image({ |
|
|
|
export default function Image({ |
|
|
|
image: { url, blurHash }, |
|
|
|
image: { url, blurHash }, |
|
|
|
alt, |
|
|
|
alt, |
|
|
|
className = '', |
|
|
|
className = '', |
|
|
|
|
|
|
|
classNames = {}, |
|
|
|
|
|
|
|
hideIfError = false, |
|
|
|
...props |
|
|
|
...props |
|
|
|
}: HTMLAttributes<HTMLDivElement> & { |
|
|
|
}: HTMLAttributes<HTMLDivElement> & { |
|
|
|
|
|
|
|
classNames?: { |
|
|
|
|
|
|
|
wrapper?: string |
|
|
|
|
|
|
|
errorPlaceholder?: string |
|
|
|
|
|
|
|
} |
|
|
|
image: TImageInfo |
|
|
|
image: TImageInfo |
|
|
|
alt?: string |
|
|
|
alt?: string |
|
|
|
|
|
|
|
hideIfError?: boolean |
|
|
|
}) { |
|
|
|
}) { |
|
|
|
const [isLoading, setIsLoading] = useState(true) |
|
|
|
const [isLoading, setIsLoading] = useState(true) |
|
|
|
const [displayBlurHash, setDisplayBlurHash] = useState(true) |
|
|
|
const [displayBlurHash, setDisplayBlurHash] = useState(true) |
|
|
|
const [blurDataUrl, setBlurDataUrl] = useState<string | null>(null) |
|
|
|
const [blurDataUrl, setBlurDataUrl] = useState<string | null>(null) |
|
|
|
|
|
|
|
const [hasError, setHasError] = useState(false) |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
useEffect(() => { |
|
|
|
if (blurHash) { |
|
|
|
if (blurHash) { |
|
|
|
@ -36,14 +45,17 @@ export default function Image({ |
|
|
|
} |
|
|
|
} |
|
|
|
}, [blurHash]) |
|
|
|
}, [blurHash]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (hideIfError && hasError) return null |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<div className={cn('relative', className)} {...props}> |
|
|
|
<div className={cn('relative', classNames.wrapper)} {...props}> |
|
|
|
{isLoading && <Skeleton className={cn('absolute inset-0', className)} />} |
|
|
|
{isLoading && <Skeleton className={cn('absolute inset-0 rounded-lg', className)} />} |
|
|
|
|
|
|
|
{!hasError ? ( |
|
|
|
<img |
|
|
|
<img |
|
|
|
src={url} |
|
|
|
src={url} |
|
|
|
alt={alt} |
|
|
|
alt={alt} |
|
|
|
className={cn( |
|
|
|
className={cn( |
|
|
|
'object-cover transition-opacity duration-300', |
|
|
|
'object-cover transition-opacity duration-300 w-full h-full', |
|
|
|
isLoading ? 'opacity-0' : 'opacity-100', |
|
|
|
isLoading ? 'opacity-0' : 'opacity-100', |
|
|
|
className |
|
|
|
className |
|
|
|
)} |
|
|
|
)} |
|
|
|
@ -51,8 +63,23 @@ export default function Image({ |
|
|
|
setIsLoading(false) |
|
|
|
setIsLoading(false) |
|
|
|
setTimeout(() => setDisplayBlurHash(false), 500) |
|
|
|
setTimeout(() => setDisplayBlurHash(false), 500) |
|
|
|
}} |
|
|
|
}} |
|
|
|
|
|
|
|
onError={() => { |
|
|
|
|
|
|
|
setIsLoading(false) |
|
|
|
|
|
|
|
setHasError(true) |
|
|
|
|
|
|
|
}} |
|
|
|
/> |
|
|
|
/> |
|
|
|
{displayBlurHash && blurDataUrl && ( |
|
|
|
) : ( |
|
|
|
|
|
|
|
<div |
|
|
|
|
|
|
|
className={cn( |
|
|
|
|
|
|
|
'object-cover flex flex-col items-center justify-center w-full h-full bg-muted', |
|
|
|
|
|
|
|
className, |
|
|
|
|
|
|
|
classNames.errorPlaceholder |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<ImageOff /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
{displayBlurHash && blurDataUrl && !hasError && ( |
|
|
|
<img |
|
|
|
<img |
|
|
|
src={blurDataUrl} |
|
|
|
src={blurDataUrl} |
|
|
|
className={cn('absolute inset-0 object-cover w-full h-full -z-10', className)} |
|
|
|
className={cn('absolute inset-0 object-cover w-full h-full -z-10', className)} |
|
|
|
|