Merge pull request #60 from sambecker/blur-fade
Fade in images after load
This commit is contained in:
commit
288e542527
@ -1,22 +1,87 @@
|
||||
'use client';
|
||||
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import { BLUR_ENABLED } from '@/site/config';
|
||||
import { clsx} from 'clsx/lite';
|
||||
import Image, { ImageProps } from 'next/image';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export default function ImageBlurFallback(props: ImageProps) {
|
||||
const {
|
||||
className,
|
||||
priority,
|
||||
blurDataURL,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [wasCached, setWasCached] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [didError, setDidError] = useState(false);
|
||||
|
||||
const [hideBlurPlaceholder, setHideBlurPlaceholder] = useState(false);
|
||||
|
||||
const imageClassName = 'object-cover h-full';
|
||||
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(
|
||||
() => setWasCached(imgRef.current?.complete ?? false),
|
||||
100,
|
||||
);
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !didError) {
|
||||
const timeout = setTimeout(() => {
|
||||
setHideBlurPlaceholder(true);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [isLoading, didError]);
|
||||
|
||||
const showPlaceholder =
|
||||
!wasCached &&
|
||||
!hideBlurPlaceholder;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
<Image {...{
|
||||
...props,
|
||||
...BLUR_ENABLED && props.blurDataURL ? {
|
||||
placeholder: 'blur',
|
||||
blurDataURL: props.blurDataURL,
|
||||
}: {
|
||||
placeholder: 'empty',
|
||||
className: clsx(
|
||||
props.className,
|
||||
'bg-gray-100/50 dark:bg-gray-900/50',
|
||||
),
|
||||
},
|
||||
}} />
|
||||
<div
|
||||
className={clsx(
|
||||
className,
|
||||
'flex relative',
|
||||
)}
|
||||
>
|
||||
{showPlaceholder &&
|
||||
<div className={clsx(
|
||||
'absolute inset-0',
|
||||
'bg-main overflow-hidden',
|
||||
'transition-opacity duration-300 ease-in',
|
||||
isLoading ? 'opacity-100' : 'opacity-0',
|
||||
)}>
|
||||
{(BLUR_ENABLED && props.blurDataURL)
|
||||
? <img {...{
|
||||
...rest,
|
||||
src: blurDataURL,
|
||||
className: clsx(
|
||||
imageClassName,
|
||||
// Fix poorly blurred placeholder data generated by Safari
|
||||
'blur-md scale-110',
|
||||
),
|
||||
}} />
|
||||
: <div className={clsx(
|
||||
'w-full h-full',
|
||||
'bg-gray-100/50 dark:bg-gray-900/50',
|
||||
)}/>}
|
||||
</div>}
|
||||
<Image {...{
|
||||
...rest,
|
||||
ref: imgRef,
|
||||
priority,
|
||||
className: imageClassName,
|
||||
onLoad: () => setIsLoading(false),
|
||||
onError: () => setDidError(true),
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,7 +20,6 @@ export default function ImageSmall({
|
||||
src,
|
||||
alt,
|
||||
blurDataURL: blurData,
|
||||
placeholder: 'blur',
|
||||
width: IMAGE_SMALL_WIDTH,
|
||||
height: Math.round(IMAGE_SMALL_WIDTH / aspectRatio),
|
||||
}} />
|
||||
|
||||
@ -19,10 +19,7 @@ export default function ImageTiny({
|
||||
className,
|
||||
src,
|
||||
alt,
|
||||
...blurData && {
|
||||
blurDataURL: blurData,
|
||||
placeholder: 'blur',
|
||||
},
|
||||
blurDataURL: blurData,
|
||||
width: IMAGE_TINY_WIDTH,
|
||||
height: Math.round(IMAGE_TINY_WIDTH / aspectRatio),
|
||||
}} />
|
||||
|
||||
@ -56,12 +56,7 @@ export default function PhotoGrid({
|
||||
<div
|
||||
key={photo.id}
|
||||
className={GRID_ASPECT_RATIO !== 0
|
||||
? clsx(
|
||||
'aspect-square',
|
||||
'overflow-hidden',
|
||||
'[&>*]:flex [&>*]:w-full [&>*]:h-full',
|
||||
'[&>*>*]:object-cover [&>*>*]:min-h-full',
|
||||
)
|
||||
? 'aspect-square overflow-hidden'
|
||||
: undefined}
|
||||
style={{
|
||||
...GRID_ASPECT_RATIO !== 0 && {
|
||||
|
||||
@ -5,8 +5,6 @@ import { clsx } from 'clsx/lite';
|
||||
import { pathForPhoto } from '@/site/paths';
|
||||
import { Camera } from '@/camera';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import AdminPhotoMenu from '@/admin/AdminPhotoMenu';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export default function PhotoSmall({
|
||||
photo,
|
||||
@ -14,34 +12,23 @@ export default function PhotoSmall({
|
||||
camera,
|
||||
simulation,
|
||||
selected,
|
||||
showAdminMenu,
|
||||
}: {
|
||||
photo: Photo
|
||||
tag?: string
|
||||
camera?: Camera
|
||||
simulation?: FilmSimulation
|
||||
selected?: boolean
|
||||
showAdminMenu?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={pathForPhoto(photo, tag, camera, simulation)}
|
||||
className={clsx(
|
||||
'relative group',
|
||||
'group',
|
||||
'flex relative w-full h-full',
|
||||
'active:brightness-75',
|
||||
selected && 'brightness-50',
|
||||
)}
|
||||
>
|
||||
<Suspense>
|
||||
{showAdminMenu &&
|
||||
<AdminPhotoMenu
|
||||
buttonClassName={clsx(
|
||||
'absolute top-1 right-1 opacity-0',
|
||||
'group-hover:opacity-100 group-focus:opacity-100',
|
||||
)}
|
||||
photo={photo}
|
||||
/>}
|
||||
</Suspense>
|
||||
<ImageSmall
|
||||
src={photo.url}
|
||||
aspectRatio={photo.aspectRatio}
|
||||
|
||||
@ -5,4 +5,4 @@ export const IMAGE_TINY_WIDTH = 50;
|
||||
export const IMAGE_SMALL_WIDTH = 300;
|
||||
|
||||
// Height determined by intrinsic photo aspect ratio
|
||||
export const IMAGE_LARGE_WIDTH = 900;
|
||||
export const IMAGE_LARGE_WIDTH = 1080;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user