Refactor infinite scroll
This commit is contained in:
parent
1dbfc2d592
commit
08451cff13
@ -35,7 +35,7 @@ export default async function GridPage() {
|
||||
photos.length > 0
|
||||
? <SiteGrid
|
||||
contentMain={<div className="space-y-0.5 sm:space-y-1">
|
||||
<PhotoGrid {...{ photos, photoPriority: true }} />
|
||||
<PhotoGrid {...{ photos }} />
|
||||
{photos.length >= INFINITE_SCROLL_MULTIPLE_GRID &&
|
||||
<InfinitePhotoScroll
|
||||
type='grid'
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { RefObject } from 'react';
|
||||
|
||||
export default function SiteGrid({
|
||||
containerRef,
|
||||
className,
|
||||
contentMain,
|
||||
contentSide,
|
||||
sideFirstOnMobile,
|
||||
sideHiddenOnMobile,
|
||||
}: {
|
||||
containerRef?: RefObject<HTMLDivElement>
|
||||
className?: string
|
||||
contentMain: JSX.Element
|
||||
contentSide?: JSX.Element
|
||||
@ -14,14 +17,17 @@ export default function SiteGrid({
|
||||
sideHiddenOnMobile?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={clsx(
|
||||
className,
|
||||
'grid',
|
||||
'grid-cols-1 md:grid-cols-12',
|
||||
'gap-x-4 lg:gap-x-6',
|
||||
'gap-y-4',
|
||||
'max-w-7xl',
|
||||
)}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
className,
|
||||
'grid',
|
||||
'grid-cols-1 md:grid-cols-12',
|
||||
'gap-x-4 lg:gap-x-6',
|
||||
'gap-y-4',
|
||||
'max-w-7xl',
|
||||
)}
|
||||
>
|
||||
<div className={clsx(
|
||||
'col-span-1 md:col-span-9',
|
||||
sideFirstOnMobile && 'order-2 md:order-none',
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { preload } from 'swr';
|
||||
import useSwrInfinite from 'swr/infinite';
|
||||
import PhotosLarge from '@/photo/PhotosLarge';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
@ -26,14 +24,10 @@ export default function InfinitePhotoScroll({
|
||||
type = 'full-frame',
|
||||
initialOffset = 0,
|
||||
itemsPerPage = 12,
|
||||
prefetch = true,
|
||||
triggerOnView = true,
|
||||
}: {
|
||||
type?: 'full-frame' | 'grid'
|
||||
initialOffset?: number
|
||||
itemsPerPage?: number
|
||||
prefetch?: boolean
|
||||
triggerOnView?: boolean
|
||||
debug?: boolean
|
||||
}) {
|
||||
const { swrTimestamp, isUserSignedIn } = useAppState();
|
||||
@ -46,20 +40,21 @@ export default function InfinitePhotoScroll({
|
||||
: [key, size]
|
||||
, [key]);
|
||||
|
||||
const fetcher = useCallback(([_key, size]: [string, number]) =>
|
||||
getPhotosAction(
|
||||
const fetcher = useCallback(([_key, size]: [string, number]) => {
|
||||
console.log('Fetching', size);
|
||||
return getPhotosAction(
|
||||
initialOffset + size * itemsPerPage,
|
||||
itemsPerPage,
|
||||
)
|
||||
, [initialOffset, itemsPerPage]);
|
||||
);
|
||||
}, [initialOffset, itemsPerPage]);
|
||||
|
||||
const { data, isLoading, isValidating, error, mutate, size, setSize } =
|
||||
const { data, isLoading, isValidating, error, mutate, setSize } =
|
||||
useSwrInfinite<Photo[]>(
|
||||
keyGenerator,
|
||||
fetcher,
|
||||
{
|
||||
revalidateFirstPage: Boolean(isUserSignedIn),
|
||||
revalidateOnMount: Boolean(isUserSignedIn),
|
||||
initialSize: 2,
|
||||
revalidateFirstPage: false,
|
||||
revalidateOnFocus: Boolean(isUserSignedIn),
|
||||
revalidateOnReconnect: Boolean(isUserSignedIn),
|
||||
},
|
||||
@ -73,46 +68,11 @@ export default function InfinitePhotoScroll({
|
||||
data && data[data.length - 1]?.length < itemsPerPage
|
||||
, [data, itemsPerPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prefetch && !isFinished) {
|
||||
preload([key, (size ?? 0) + 1], fetcher);
|
||||
}
|
||||
}, [prefetch, isFinished, key, size, fetcher]);
|
||||
|
||||
const advance = useCallback(() => {
|
||||
if (!isFinished && !isLoadingOrValidating) {
|
||||
setSize(size => size + 1);
|
||||
}
|
||||
}, [isFinished, setSize, isLoadingOrValidating]);
|
||||
|
||||
useEffect(() => {
|
||||
// Only add observer if button is rendered
|
||||
if (buttonContainerRef.current) {
|
||||
const observer = new IntersectionObserver(e => {
|
||||
if (triggerOnView && e[0].isIntersecting) {
|
||||
advance();
|
||||
}
|
||||
}, {
|
||||
root: null,
|
||||
threshold: 0,
|
||||
});
|
||||
observer.observe(buttonContainerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
}, [triggerOnView, advance]);
|
||||
|
||||
// Poll for button getting stuck
|
||||
useEffect(() => {
|
||||
if (triggerOnView && !isFinished && !isLoadingOrValidating) {
|
||||
const interval = setInterval(() => {
|
||||
const rect = buttonContainerRef.current?.getBoundingClientRect();
|
||||
if (rect && rect.top <= window.innerHeight) {
|
||||
advance();
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [advance, isFinished, isLoadingOrValidating, triggerOnView]);
|
||||
}, [isFinished, isLoadingOrValidating, setSize]);
|
||||
|
||||
const photos = useMemo(() => (data ?? [])?.flat(), [data]);
|
||||
|
||||
@ -128,11 +88,7 @@ export default function InfinitePhotoScroll({
|
||||
} as any), [data, mutate]);
|
||||
|
||||
const renderMoreButton = () =>
|
||||
<div
|
||||
ref={buttonContainerRef}
|
||||
// Make button bounding visible earlier
|
||||
className="-translate-y-32 pt-32 -mb-32"
|
||||
>
|
||||
<div ref={buttonContainerRef}>
|
||||
<button
|
||||
onClick={() => error ? mutate() : advance()}
|
||||
disabled={isLoading || isValidating}
|
||||
@ -152,8 +108,15 @@ export default function InfinitePhotoScroll({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{type === 'full-frame'
|
||||
? <PhotosLarge {...{ photos, revalidatePhoto }} />
|
||||
: <PhotoGrid {...{ photos }} />}
|
||||
? <PhotosLarge {...{
|
||||
photos,
|
||||
revalidatePhoto,
|
||||
onLastPhotoVisible: advance,
|
||||
}} />
|
||||
: <PhotoGrid {...{
|
||||
photos,
|
||||
onLastPhotoVisible: advance,
|
||||
}} />}
|
||||
{!isFinished && (type === 'full-frame'
|
||||
? <SiteGrid contentMain={renderMoreButton()} />
|
||||
: renderMoreButton())}
|
||||
|
||||
@ -19,6 +19,7 @@ export default function PhotoGrid({
|
||||
staggerOnFirstLoadOnly = true,
|
||||
additionalTile,
|
||||
small,
|
||||
onLastPhotoVisible,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
selectedPhoto?: Photo
|
||||
@ -33,6 +34,7 @@ export default function PhotoGrid({
|
||||
showMorePath?: string
|
||||
additionalTile?: JSX.Element
|
||||
small?: boolean
|
||||
onLastPhotoVisible?: () => void
|
||||
}) {
|
||||
return (
|
||||
<AnimateItems
|
||||
@ -51,7 +53,7 @@ export default function PhotoGrid({
|
||||
distanceOffset={40}
|
||||
animateOnFirstLoadOnly={animateOnFirstLoadOnly}
|
||||
staggerOnFirstLoadOnly={staggerOnFirstLoadOnly}
|
||||
items={photos.map(photo =>
|
||||
items={photos.map((photo, index) =>
|
||||
<div
|
||||
key={photo.id}
|
||||
className={GRID_ASPECT_RATIO !== 0
|
||||
@ -70,6 +72,9 @@ export default function PhotoGrid({
|
||||
simulation,
|
||||
selected: photo.id === selectedPhoto?.id,
|
||||
priority: photoPriority,
|
||||
onVisible: index === photos.length - 1
|
||||
? onLastPhotoVisible
|
||||
: undefined,
|
||||
}} />
|
||||
</div>).concat(additionalTile ?? [])}
|
||||
itemKeys={photos.map(photo => photo.id)
|
||||
|
||||
@ -22,6 +22,7 @@ import PhotoLink from './PhotoLink';
|
||||
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
|
||||
import AdminPhotoMenuClient from '@/admin/AdminPhotoMenuClient';
|
||||
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function PhotoLarge({
|
||||
photo,
|
||||
@ -36,6 +37,7 @@ export default function PhotoLarge({
|
||||
shouldShareCamera,
|
||||
shouldShareSimulation,
|
||||
shouldScrollOnShare,
|
||||
onVisible,
|
||||
}: {
|
||||
photo: Photo
|
||||
primaryTag?: string
|
||||
@ -49,7 +51,10 @@ export default function PhotoLarge({
|
||||
shouldShareCamera?: boolean
|
||||
shouldShareSimulation?: boolean
|
||||
shouldScrollOnShare?: boolean
|
||||
onVisible?: () => void
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const tags = sortTags(photo.tags, primaryTag);
|
||||
|
||||
const camera = cameraFromPhoto(photo);
|
||||
@ -58,8 +63,24 @@ export default function PhotoLarge({
|
||||
const showTagsContent = tags.length > 0;
|
||||
const showExifContent = shouldShowExifDataForPhoto(photo);
|
||||
|
||||
useEffect(() => {
|
||||
if (onVisible && ref.current) {
|
||||
const observer = new IntersectionObserver(e => {
|
||||
if (e[0].isIntersecting) {
|
||||
onVisible();
|
||||
}
|
||||
}, {
|
||||
root: null,
|
||||
threshold: 0,
|
||||
});
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
}, [onVisible]);
|
||||
|
||||
return (
|
||||
<SiteGrid
|
||||
containerRef={ref}
|
||||
contentMain={
|
||||
<Link
|
||||
href={pathForPhoto(photo)}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { Photo, altTextForPhoto } from '.';
|
||||
import ImageSmall from '@/components/ImageSmall';
|
||||
import Link from 'next/link';
|
||||
@ -6,6 +8,7 @@ import { pathForPhoto } from '@/site/paths';
|
||||
import { Camera } from '@/camera';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function PhotoSmall({
|
||||
photo,
|
||||
@ -15,6 +18,7 @@ export default function PhotoSmall({
|
||||
selected,
|
||||
priority,
|
||||
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
||||
onVisible,
|
||||
}: {
|
||||
photo: Photo
|
||||
tag?: string
|
||||
@ -23,9 +27,28 @@ export default function PhotoSmall({
|
||||
selected?: boolean
|
||||
priority?: boolean
|
||||
prefetch?: boolean
|
||||
onVisible?: () => void
|
||||
}) {
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (onVisible && ref.current) {
|
||||
const observer = new IntersectionObserver(e => {
|
||||
if (e[0].isIntersecting) {
|
||||
onVisible();
|
||||
}
|
||||
}, {
|
||||
root: null,
|
||||
threshold: 0,
|
||||
});
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
}, [onVisible]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
href={pathForPhoto(photo, tag, camera, simulation)}
|
||||
className={clsx(
|
||||
'flex w-full h-full',
|
||||
|
||||
@ -8,11 +8,13 @@ export default function PhotosLarge({
|
||||
animate = true,
|
||||
prefetchFirstPhotoLinks,
|
||||
revalidatePhoto,
|
||||
onLastPhotoVisible,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
animate?: boolean
|
||||
prefetchFirstPhotoLinks?: boolean
|
||||
revalidatePhoto?: RevalidatePhoto
|
||||
onLastPhotoVisible?: () => void
|
||||
}) {
|
||||
return (
|
||||
<AnimateItems
|
||||
@ -29,6 +31,9 @@ export default function PhotosLarge({
|
||||
priority={index <= 1}
|
||||
prefetchRelatedLinks={prefetchFirstPhotoLinks && index === 0}
|
||||
revalidatePhoto={revalidatePhoto}
|
||||
onVisible={index === photos.length - 1
|
||||
? onLastPhotoVisible
|
||||
: undefined}
|
||||
/>)}
|
||||
itemKeys={photos.map(photo => photo.id)}
|
||||
/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user