From 6e7e46d6029bf676b7936cef4a2c6ec09ea7e699 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 28 Apr 2024 17:36:20 -0500 Subject: [PATCH] Refactor infinite scroll pattern, use for admin photos --- src/admin/AdminPhotoTable.tsx | 17 ++++++++-- src/admin/AdminPhotoTableInfinite.tsx | 28 +++++++++++++++ src/app/admin/photos/page.tsx | 34 ++++++++----------- src/app/grid/page.tsx | 4 +-- src/app/page.tsx | 5 ++- src/photo/InfinitePhotoScroll.tsx | 8 +++-- ...toScrollGrid.tsx => PhotoGridInfinite.tsx} | 11 +++--- src/photo/PhotoLarge.tsx | 18 ++-------- src/photo/PhotoSmall.tsx | 18 ++-------- src/photo/PhotoTiny.tsx | 9 +++++ src/photo/PhotosLarge.tsx | 4 +-- ...hotosLarge.tsx => PhotosLargeInfinite.tsx} | 12 +++---- src/photo/actions.ts | 16 ++++++--- src/utility/useOnVisible.ts | 21 ++++++++++++ 14 files changed, 128 insertions(+), 77 deletions(-) create mode 100644 src/admin/AdminPhotoTableInfinite.tsx rename src/photo/{InfinitePhotoScrollGrid.tsx => PhotoGridInfinite.tsx} (71%) rename src/photo/{InfinitePhotoScrollPhotosLarge.tsx => PhotosLargeInfinite.tsx} (71%) create mode 100644 src/utility/useOnVisible.ts diff --git a/src/admin/AdminPhotoTable.tsx b/src/admin/AdminPhotoTable.tsx index 4bdb4d2e..4354bf8c 100644 --- a/src/admin/AdminPhotoTable.tsx +++ b/src/admin/AdminPhotoTable.tsx @@ -19,19 +19,29 @@ import { syncPhotoExifDataAction, } from '@/photo/actions'; import { useAppState } from '@/state/AppState'; +import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll'; export default function AdminPhotoTable({ photos, + onLastPhotoVisible, + revalidatePhoto, }: { photos: Photo[], + onLastPhotoVisible?: () => void + revalidatePhoto?: RevalidatePhoto }) { const { invalidateSwr } = useAppState(); return ( - {photos.map(photo => + {photos.map((photo, index) => - +
- {photo.title || 'Untitled'} + {titleForPhoto(photo)} {photo.hidden && revalidatePhoto?.(photo.id, true)} > diff --git a/src/admin/AdminPhotoTableInfinite.tsx b/src/admin/AdminPhotoTableInfinite.tsx new file mode 100644 index 00000000..ab8bf1e9 --- /dev/null +++ b/src/admin/AdminPhotoTableInfinite.tsx @@ -0,0 +1,28 @@ +'use client'; + +import InfinitePhotoScroll, { + InfinitePhotoScrollExternalProps, +} from '../photo/InfinitePhotoScroll'; +import AdminPhotoTable from './AdminPhotoTable'; + +export default function AdminPhotoTableInfinite({ + initialOffset, + itemsPerPage, +}: InfinitePhotoScrollExternalProps) { + return ( + + {({ photos, onLastPhotoVisible, revalidatePhoto }) => + } + + ); +} diff --git a/src/app/admin/photos/page.tsx b/src/app/admin/photos/page.tsx index d781ac01..f25f965c 100644 --- a/src/app/admin/photos/page.tsx +++ b/src/app/admin/photos/page.tsx @@ -1,40 +1,36 @@ import PhotoUpload from '@/photo/PhotoUpload'; import { clsx } from 'clsx/lite'; import SiteGrid from '@/components/SiteGrid'; -import { pathForAdminPhotos } from '@/site/paths'; import { getPhotosCountIncludingHiddenCached } from '@/photo/cache'; -import { - PaginationParams, - getPaginationFromSearchParams, -} from '@/site/pagination'; import StorageUrls from '@/admin/StorageUrls'; import { PRO_MODE_ENABLED } from '@/site/config'; import { getStoragePhotoUrlsNoStore } from '@/services/storage/cache'; -import MoreComponentsFromSearchParams from - '@/components/MoreComponentsFromSearchParams'; import { getPhotos } from '@/services/vercel-postgres'; import { revalidatePath } from 'next/cache'; import AdminPhotoTable from '@/admin/AdminPhotoTable'; +import AdminPhotoTableInfinite from + '@/admin/AdminPhotoTableInfinite'; const DEBUG_PHOTO_BLOBS = false; -export default async function AdminPhotosPage({ - searchParams, -}: PaginationParams) { - const { offset, limit } = getPaginationFromSearchParams(searchParams); +const INFINITE_SCROLL_INITIAL_ADMIN_PHOTOS = 25; +const INFINITE_SCROLL_MULTIPLE_ADMIN_PHOTOS = 50; +export default async function AdminPhotosPage() { const [ photos, - count, + photosCount, blobPhotoUrls, ] = await Promise.all([ - getPhotos({ includeHidden: true, sortBy: 'createdAt', limit }), + getPhotos({ + includeHidden: true, + sortBy: 'createdAt', + limit: INFINITE_SCROLL_INITIAL_ADMIN_PHOTOS, + }), getPhotosCountIncludingHiddenCached(), DEBUG_PHOTO_BLOBS ? getStoragePhotoUrlsNoStore() : [], ]); - const showMorePhotos = count > photos.length; - return ( }
- {showMorePhotos && - photos.length && + }
} diff --git a/src/app/grid/page.tsx b/src/app/grid/page.tsx index a9db1275..11cfd6bc 100644 --- a/src/app/grid/page.tsx +++ b/src/app/grid/page.tsx @@ -12,7 +12,7 @@ import PhotoGridSidebar from '@/photo/PhotoGridSidebar'; import { getPhotoSidebarData } from '@/photo/data'; import { getPhotos } from '@/services/vercel-postgres'; import { cache } from 'react'; -import InfinitePhotoScrollGrid from '@/photo/InfinitePhotoScrollGrid'; +import PhotoGridInfinite from '@/photo/PhotoGridInfinite'; export const dynamic = 'force-static'; @@ -41,7 +41,7 @@ export default async function GridPage() { contentMain={
{photosCount > photos.length && - } diff --git a/src/app/page.tsx b/src/app/page.tsx index a4bfc36b..de27623f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,8 +9,7 @@ import { MAX_PHOTOS_TO_SHOW_OG } from '@/image-response'; import PhotosLarge from '@/photo/PhotosLarge'; import { cache } from 'react'; import { getPhotos, getPhotosCount } from '@/services/vercel-postgres'; -import InfinitePhotoScrollPhotosLarge from - '@/photo/InfinitePhotoScrollPhotosLarge'; +import PhotosLargeInfinite from '@/photo/PhotosLargeInfinite'; export const dynamic = 'force-static'; @@ -44,7 +43,7 @@ export default async function HomePage() { ?
{photosCount > photos.length && - } diff --git a/src/photo/InfinitePhotoScroll.tsx b/src/photo/InfinitePhotoScroll.tsx index a1c728ea..64cf4df8 100644 --- a/src/photo/InfinitePhotoScroll.tsx +++ b/src/photo/InfinitePhotoScroll.tsx @@ -30,11 +30,13 @@ export default function InfinitePhotoScroll({ itemsPerPage, wrapMoreButtonInGrid, useCachedPhotos = true, + includeHiddenPhotos, children, }: InfinitePhotoScrollExternalProps & { cacheKey: string - wrapMoreButtonInGrid: boolean + wrapMoreButtonInGrid?: boolean useCachedPhotos?: boolean + includeHiddenPhotos?: boolean children: (props: { photos: Photo[] onLastPhotoVisible: () => void @@ -56,12 +58,14 @@ export default function InfinitePhotoScroll({ ? getPhotosCachedAction( initialOffset + size * itemsPerPage, itemsPerPage, + includeHiddenPhotos, ) : getPhotosAction( initialOffset + size * itemsPerPage, itemsPerPage, + includeHiddenPhotos, ) - , [useCachedPhotos, initialOffset, itemsPerPage]); + , [useCachedPhotos, initialOffset, itemsPerPage, includeHiddenPhotos]); const { data, isLoading, isValidating, error, mutate, setSize } = useSwrInfinite( diff --git a/src/photo/InfinitePhotoScrollGrid.tsx b/src/photo/PhotoGridInfinite.tsx similarity index 71% rename from src/photo/InfinitePhotoScrollGrid.tsx rename to src/photo/PhotoGridInfinite.tsx index d3ec43dd..ffde37c6 100644 --- a/src/photo/InfinitePhotoScrollGrid.tsx +++ b/src/photo/PhotoGridInfinite.tsx @@ -5,7 +5,7 @@ import InfinitePhotoScroll, { } from './InfinitePhotoScroll'; import PhotoGrid from './PhotoGrid'; -export default function InfinitePhotoScrollGrid({ +export default function PhotoGridInfinite({ initialOffset, itemsPerPage, }: InfinitePhotoScrollExternalProps) { @@ -14,13 +14,12 @@ export default function InfinitePhotoScrollGrid({ cacheKey="Grid" initialOffset={initialOffset} itemsPerPage={itemsPerPage} - wrapMoreButtonInGrid={false} > {({ photos, onLastPhotoVisible }) => - } + } ); } diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index fff7c8b6..fdf755e5 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -22,7 +22,8 @@ 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'; +import { useRef } from 'react'; +import useOnVisible from '@/utility/useOnVisible'; export default function PhotoLarge({ photo, @@ -63,20 +64,7 @@ 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]); + useOnVisible(ref, onVisible); return ( (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]); + useOnVisible(ref, onVisible); return ( void }) { + const ref = useRef(null); + + useOnVisible(ref, onVisible); + return ( void + revalidatePhoto?: RevalidatePhoto }) { return ( {({ photos, onLastPhotoVisible, revalidatePhoto }) => - } + } ); } diff --git a/src/photo/actions.ts b/src/photo/actions.ts index fac16382..030b4e22 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -195,11 +195,19 @@ export async function streamAiImageQueryAction( streamOpenAiImageQuery(imageBase64, AI_IMAGE_QUERIES[query])); } -export const getPhotosCachedAction = async (offset: number, limit: number) => - getPhotosCachedCached({ offset, limit }); +export const getPhotosCachedAction = async ( + offset: number, + limit: number, + includeHidden?: boolean, +) => + getPhotosCachedCached({ offset, includeHidden, limit }); -export const getPhotosAction = async (offset: number, limit: number) => - getPhotos({ offset, limit }); +export const getPhotosAction = async ( + offset: number, + limit: number, + includeHidden?: boolean, +) => + getPhotos({ offset, includeHidden, limit }); export const queryPhotosByTitleAction = async (query: string) => (await getPhotos({ query, limit: 10 })) diff --git a/src/utility/useOnVisible.ts b/src/utility/useOnVisible.ts new file mode 100644 index 00000000..97a35406 --- /dev/null +++ b/src/utility/useOnVisible.ts @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; + +export default function useOnVisible( + ref: React.RefObject, + onVisible?: () => void +) { + 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(); + } + }, [ref, onVisible]); +}