Refactor infinite scroll pattern, use for admin photos
This commit is contained in:
parent
eb59e58b1c
commit
6e7e46d602
@ -19,19 +19,29 @@ import {
|
|||||||
syncPhotoExifDataAction,
|
syncPhotoExifDataAction,
|
||||||
} from '@/photo/actions';
|
} from '@/photo/actions';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
|
import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
|
||||||
|
|
||||||
export default function AdminPhotoTable({
|
export default function AdminPhotoTable({
|
||||||
photos,
|
photos,
|
||||||
|
onLastPhotoVisible,
|
||||||
|
revalidatePhoto,
|
||||||
}: {
|
}: {
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
onLastPhotoVisible?: () => void
|
||||||
|
revalidatePhoto?: RevalidatePhoto
|
||||||
}) {
|
}) {
|
||||||
const { invalidateSwr } = useAppState();
|
const { invalidateSwr } = useAppState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminTable>
|
<AdminTable>
|
||||||
{photos.map(photo =>
|
{photos.map((photo, index) =>
|
||||||
<Fragment key={photo.id}>
|
<Fragment key={photo.id}>
|
||||||
<PhotoTiny photo={photo} />
|
<PhotoTiny
|
||||||
|
photo={photo}
|
||||||
|
onVisible={index === photos.length - 1
|
||||||
|
? onLastPhotoVisible
|
||||||
|
: undefined}
|
||||||
|
/>
|
||||||
<div className="flex flex-col lg:flex-row">
|
<div className="flex flex-col lg:flex-row">
|
||||||
<Link
|
<Link
|
||||||
key={photo.id}
|
key={photo.id}
|
||||||
@ -43,7 +53,7 @@ export default function AdminPhotoTable({
|
|||||||
'inline-flex items-center gap-2',
|
'inline-flex items-center gap-2',
|
||||||
photo.hidden && 'text-dim',
|
photo.hidden && 'text-dim',
|
||||||
)}>
|
)}>
|
||||||
<span>{photo.title || 'Untitled'}</span>
|
<span>{titleForPhoto(photo)}</span>
|
||||||
{photo.hidden &&
|
{photo.hidden &&
|
||||||
<AiOutlineEyeInvisible
|
<AiOutlineEyeInvisible
|
||||||
className="translate-y-[0.25px]"
|
className="translate-y-[0.25px]"
|
||||||
@ -91,6 +101,7 @@ export default function AdminPhotoTable({
|
|||||||
<FormWithConfirm
|
<FormWithConfirm
|
||||||
action={deletePhotoFormAction}
|
action={deletePhotoFormAction}
|
||||||
confirmText={deleteConfirmationTextForPhoto(photo)}
|
confirmText={deleteConfirmationTextForPhoto(photo)}
|
||||||
|
onSubmit={() => revalidatePhoto?.(photo.id, true)}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={photo.id} />
|
<input type="hidden" name="id" value={photo.id} />
|
||||||
<input type="hidden" name="url" value={photo.url} />
|
<input type="hidden" name="url" value={photo.url} />
|
||||||
|
|||||||
28
src/admin/AdminPhotoTableInfinite.tsx
Normal file
28
src/admin/AdminPhotoTableInfinite.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import InfinitePhotoScroll, {
|
||||||
|
InfinitePhotoScrollExternalProps,
|
||||||
|
} from '../photo/InfinitePhotoScroll';
|
||||||
|
import AdminPhotoTable from './AdminPhotoTable';
|
||||||
|
|
||||||
|
export default function AdminPhotoTableInfinite({
|
||||||
|
initialOffset,
|
||||||
|
itemsPerPage,
|
||||||
|
}: InfinitePhotoScrollExternalProps) {
|
||||||
|
return (
|
||||||
|
<InfinitePhotoScroll
|
||||||
|
cacheKey="AdminPhotoTable"
|
||||||
|
initialOffset={initialOffset}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
useCachedPhotos={false}
|
||||||
|
includeHiddenPhotos
|
||||||
|
>
|
||||||
|
{({ photos, onLastPhotoVisible, revalidatePhoto }) =>
|
||||||
|
<AdminPhotoTable
|
||||||
|
photos={photos}
|
||||||
|
onLastPhotoVisible={onLastPhotoVisible}
|
||||||
|
revalidatePhoto={revalidatePhoto}
|
||||||
|
/>}
|
||||||
|
</InfinitePhotoScroll>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,40 +1,36 @@
|
|||||||
import PhotoUpload from '@/photo/PhotoUpload';
|
import PhotoUpload from '@/photo/PhotoUpload';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import { pathForAdminPhotos } from '@/site/paths';
|
|
||||||
import { getPhotosCountIncludingHiddenCached } from '@/photo/cache';
|
import { getPhotosCountIncludingHiddenCached } from '@/photo/cache';
|
||||||
import {
|
|
||||||
PaginationParams,
|
|
||||||
getPaginationFromSearchParams,
|
|
||||||
} from '@/site/pagination';
|
|
||||||
import StorageUrls from '@/admin/StorageUrls';
|
import StorageUrls from '@/admin/StorageUrls';
|
||||||
import { PRO_MODE_ENABLED } from '@/site/config';
|
import { PRO_MODE_ENABLED } from '@/site/config';
|
||||||
import { getStoragePhotoUrlsNoStore } from '@/services/storage/cache';
|
import { getStoragePhotoUrlsNoStore } from '@/services/storage/cache';
|
||||||
import MoreComponentsFromSearchParams from
|
|
||||||
'@/components/MoreComponentsFromSearchParams';
|
|
||||||
import { getPhotos } from '@/services/vercel-postgres';
|
import { getPhotos } from '@/services/vercel-postgres';
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import AdminPhotoTable from '@/admin/AdminPhotoTable';
|
import AdminPhotoTable from '@/admin/AdminPhotoTable';
|
||||||
|
import AdminPhotoTableInfinite from
|
||||||
|
'@/admin/AdminPhotoTableInfinite';
|
||||||
|
|
||||||
const DEBUG_PHOTO_BLOBS = false;
|
const DEBUG_PHOTO_BLOBS = false;
|
||||||
|
|
||||||
export default async function AdminPhotosPage({
|
const INFINITE_SCROLL_INITIAL_ADMIN_PHOTOS = 25;
|
||||||
searchParams,
|
const INFINITE_SCROLL_MULTIPLE_ADMIN_PHOTOS = 50;
|
||||||
}: PaginationParams) {
|
|
||||||
const { offset, limit } = getPaginationFromSearchParams(searchParams);
|
|
||||||
|
|
||||||
|
export default async function AdminPhotosPage() {
|
||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
count,
|
photosCount,
|
||||||
blobPhotoUrls,
|
blobPhotoUrls,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotos({ includeHidden: true, sortBy: 'createdAt', limit }),
|
getPhotos({
|
||||||
|
includeHidden: true,
|
||||||
|
sortBy: 'createdAt',
|
||||||
|
limit: INFINITE_SCROLL_INITIAL_ADMIN_PHOTOS,
|
||||||
|
}),
|
||||||
getPhotosCountIncludingHiddenCached(),
|
getPhotosCountIncludingHiddenCached(),
|
||||||
DEBUG_PHOTO_BLOBS ? getStoragePhotoUrlsNoStore() : [],
|
DEBUG_PHOTO_BLOBS ? getStoragePhotoUrlsNoStore() : [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const showMorePhotos = count > photos.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteGrid
|
<SiteGrid
|
||||||
contentMain={
|
contentMain={
|
||||||
@ -59,10 +55,10 @@ export default async function AdminPhotosPage({
|
|||||||
</div>}
|
</div>}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<AdminPhotoTable photos={photos} />
|
<AdminPhotoTable photos={photos} />
|
||||||
{showMorePhotos &&
|
{photosCount > photos.length &&
|
||||||
<MoreComponentsFromSearchParams
|
<AdminPhotoTableInfinite
|
||||||
label="More photos"
|
initialOffset={INFINITE_SCROLL_INITIAL_ADMIN_PHOTOS}
|
||||||
path={pathForAdminPhotos(offset + 1)}
|
itemsPerPage={INFINITE_SCROLL_MULTIPLE_ADMIN_PHOTOS}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import PhotoGridSidebar from '@/photo/PhotoGridSidebar';
|
|||||||
import { getPhotoSidebarData } from '@/photo/data';
|
import { getPhotoSidebarData } from '@/photo/data';
|
||||||
import { getPhotos } from '@/services/vercel-postgres';
|
import { getPhotos } from '@/services/vercel-postgres';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import InfinitePhotoScrollGrid from '@/photo/InfinitePhotoScrollGrid';
|
import PhotoGridInfinite from '@/photo/PhotoGridInfinite';
|
||||||
|
|
||||||
export const dynamic = 'force-static';
|
export const dynamic = 'force-static';
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ export default async function GridPage() {
|
|||||||
contentMain={<div className="space-y-0.5 sm:space-y-1">
|
contentMain={<div className="space-y-0.5 sm:space-y-1">
|
||||||
<PhotoGrid {...{ photos }} />
|
<PhotoGrid {...{ photos }} />
|
||||||
{photosCount > photos.length &&
|
{photosCount > photos.length &&
|
||||||
<InfinitePhotoScrollGrid
|
<PhotoGridInfinite
|
||||||
initialOffset={INFINITE_SCROLL_INITIAL_GRID}
|
initialOffset={INFINITE_SCROLL_INITIAL_GRID}
|
||||||
itemsPerPage={INFINITE_SCROLL_MULTIPLE_GRID}
|
itemsPerPage={INFINITE_SCROLL_MULTIPLE_GRID}
|
||||||
/>}
|
/>}
|
||||||
|
|||||||
@ -9,8 +9,7 @@ import { MAX_PHOTOS_TO_SHOW_OG } from '@/image-response';
|
|||||||
import PhotosLarge from '@/photo/PhotosLarge';
|
import PhotosLarge from '@/photo/PhotosLarge';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import { getPhotos, getPhotosCount } from '@/services/vercel-postgres';
|
import { getPhotos, getPhotosCount } from '@/services/vercel-postgres';
|
||||||
import InfinitePhotoScrollPhotosLarge from
|
import PhotosLargeInfinite from '@/photo/PhotosLargeInfinite';
|
||||||
'@/photo/InfinitePhotoScrollPhotosLarge';
|
|
||||||
|
|
||||||
export const dynamic = 'force-static';
|
export const dynamic = 'force-static';
|
||||||
|
|
||||||
@ -44,7 +43,7 @@ export default async function HomePage() {
|
|||||||
? <div className="space-y-1">
|
? <div className="space-y-1">
|
||||||
<PhotosLarge {...{ photos }} />
|
<PhotosLarge {...{ photos }} />
|
||||||
{photosCount > photos.length &&
|
{photosCount > photos.length &&
|
||||||
<InfinitePhotoScrollPhotosLarge
|
<PhotosLargeInfinite
|
||||||
initialOffset={INFINITE_SCROLL_INITIAL_HOME}
|
initialOffset={INFINITE_SCROLL_INITIAL_HOME}
|
||||||
itemsPerPage={INFINITE_SCROLL_MULTIPLE_HOME}
|
itemsPerPage={INFINITE_SCROLL_MULTIPLE_HOME}
|
||||||
/>}
|
/>}
|
||||||
|
|||||||
@ -30,11 +30,13 @@ export default function InfinitePhotoScroll({
|
|||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
wrapMoreButtonInGrid,
|
wrapMoreButtonInGrid,
|
||||||
useCachedPhotos = true,
|
useCachedPhotos = true,
|
||||||
|
includeHiddenPhotos,
|
||||||
children,
|
children,
|
||||||
}: InfinitePhotoScrollExternalProps & {
|
}: InfinitePhotoScrollExternalProps & {
|
||||||
cacheKey: string
|
cacheKey: string
|
||||||
wrapMoreButtonInGrid: boolean
|
wrapMoreButtonInGrid?: boolean
|
||||||
useCachedPhotos?: boolean
|
useCachedPhotos?: boolean
|
||||||
|
includeHiddenPhotos?: boolean
|
||||||
children: (props: {
|
children: (props: {
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
onLastPhotoVisible: () => void
|
onLastPhotoVisible: () => void
|
||||||
@ -56,12 +58,14 @@ export default function InfinitePhotoScroll({
|
|||||||
? getPhotosCachedAction(
|
? getPhotosCachedAction(
|
||||||
initialOffset + size * itemsPerPage,
|
initialOffset + size * itemsPerPage,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
|
includeHiddenPhotos,
|
||||||
)
|
)
|
||||||
: getPhotosAction(
|
: getPhotosAction(
|
||||||
initialOffset + size * itemsPerPage,
|
initialOffset + size * itemsPerPage,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
|
includeHiddenPhotos,
|
||||||
)
|
)
|
||||||
, [useCachedPhotos, initialOffset, itemsPerPage]);
|
, [useCachedPhotos, initialOffset, itemsPerPage, includeHiddenPhotos]);
|
||||||
|
|
||||||
const { data, isLoading, isValidating, error, mutate, setSize } =
|
const { data, isLoading, isValidating, error, mutate, setSize } =
|
||||||
useSwrInfinite<Photo[]>(
|
useSwrInfinite<Photo[]>(
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import InfinitePhotoScroll, {
|
|||||||
} from './InfinitePhotoScroll';
|
} from './InfinitePhotoScroll';
|
||||||
import PhotoGrid from './PhotoGrid';
|
import PhotoGrid from './PhotoGrid';
|
||||||
|
|
||||||
export default function InfinitePhotoScrollGrid({
|
export default function PhotoGridInfinite({
|
||||||
initialOffset,
|
initialOffset,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
}: InfinitePhotoScrollExternalProps) {
|
}: InfinitePhotoScrollExternalProps) {
|
||||||
@ -14,13 +14,12 @@ export default function InfinitePhotoScrollGrid({
|
|||||||
cacheKey="Grid"
|
cacheKey="Grid"
|
||||||
initialOffset={initialOffset}
|
initialOffset={initialOffset}
|
||||||
itemsPerPage={itemsPerPage}
|
itemsPerPage={itemsPerPage}
|
||||||
wrapMoreButtonInGrid={false}
|
|
||||||
>
|
>
|
||||||
{({ photos, onLastPhotoVisible }) =>
|
{({ photos, onLastPhotoVisible }) =>
|
||||||
<PhotoGrid {...{
|
<PhotoGrid
|
||||||
photos,
|
photos={photos}
|
||||||
onLastPhotoVisible,
|
onLastPhotoVisible={onLastPhotoVisible}
|
||||||
}} />}
|
/>}
|
||||||
</InfinitePhotoScroll>
|
</InfinitePhotoScroll>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -22,7 +22,8 @@ import PhotoLink from './PhotoLink';
|
|||||||
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
|
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
|
||||||
import AdminPhotoMenuClient from '@/admin/AdminPhotoMenuClient';
|
import AdminPhotoMenuClient from '@/admin/AdminPhotoMenuClient';
|
||||||
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import useOnVisible from '@/utility/useOnVisible';
|
||||||
|
|
||||||
export default function PhotoLarge({
|
export default function PhotoLarge({
|
||||||
photo,
|
photo,
|
||||||
@ -63,20 +64,7 @@ export default function PhotoLarge({
|
|||||||
const showTagsContent = tags.length > 0;
|
const showTagsContent = tags.length > 0;
|
||||||
const showExifContent = shouldShowExifDataForPhoto(photo);
|
const showExifContent = shouldShowExifDataForPhoto(photo);
|
||||||
|
|
||||||
useEffect(() => {
|
useOnVisible(ref, onVisible);
|
||||||
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 (
|
return (
|
||||||
<SiteGrid
|
<SiteGrid
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import { pathForPhoto } from '@/site/paths';
|
|||||||
import { Camera } from '@/camera';
|
import { Camera } from '@/camera';
|
||||||
import { FilmSimulation } from '@/simulation';
|
import { FilmSimulation } from '@/simulation';
|
||||||
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
|
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import useOnVisible from '@/utility/useOnVisible';
|
||||||
|
|
||||||
export default function PhotoSmall({
|
export default function PhotoSmall({
|
||||||
photo,
|
photo,
|
||||||
@ -31,20 +32,7 @@ export default function PhotoSmall({
|
|||||||
}) {
|
}) {
|
||||||
const ref = useRef<HTMLAnchorElement>(null);
|
const ref = useRef<HTMLAnchorElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useOnVisible(ref, onVisible);
|
||||||
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 (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import Link from 'next/link';
|
|||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { pathForPhoto } from '@/site/paths';
|
import { pathForPhoto } from '@/site/paths';
|
||||||
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
|
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import useOnVisible from '@/utility/useOnVisible';
|
||||||
|
|
||||||
export default function PhotoTiny({
|
export default function PhotoTiny({
|
||||||
photo,
|
photo,
|
||||||
@ -11,15 +13,22 @@ export default function PhotoTiny({
|
|||||||
selected,
|
selected,
|
||||||
className,
|
className,
|
||||||
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
||||||
|
onVisible,
|
||||||
}: {
|
}: {
|
||||||
photo: Photo
|
photo: Photo
|
||||||
tag?: string
|
tag?: string
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
prefetch?: boolean
|
prefetch?: boolean
|
||||||
|
onVisible?: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const ref = useRef<HTMLAnchorElement>(null);
|
||||||
|
|
||||||
|
useOnVisible(ref, onVisible);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
ref={ref}
|
||||||
href={pathForPhoto(photo, tag)}
|
href={pathForPhoto(photo, tag)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
className,
|
className,
|
||||||
|
|||||||
@ -7,14 +7,14 @@ export default function PhotosLarge({
|
|||||||
photos,
|
photos,
|
||||||
animate = true,
|
animate = true,
|
||||||
prefetchFirstPhotoLinks,
|
prefetchFirstPhotoLinks,
|
||||||
revalidatePhoto,
|
|
||||||
onLastPhotoVisible,
|
onLastPhotoVisible,
|
||||||
|
revalidatePhoto,
|
||||||
}: {
|
}: {
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
animate?: boolean
|
animate?: boolean
|
||||||
prefetchFirstPhotoLinks?: boolean
|
prefetchFirstPhotoLinks?: boolean
|
||||||
revalidatePhoto?: RevalidatePhoto
|
|
||||||
onLastPhotoVisible?: () => void
|
onLastPhotoVisible?: () => void
|
||||||
|
revalidatePhoto?: RevalidatePhoto
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<AnimateItems
|
<AnimateItems
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import InfinitePhotoScroll, {
|
|||||||
} from './InfinitePhotoScroll';
|
} from './InfinitePhotoScroll';
|
||||||
import PhotosLarge from './PhotosLarge';
|
import PhotosLarge from './PhotosLarge';
|
||||||
|
|
||||||
export default function InfinitePhotoScrollPhotosLarge({
|
export default function PhotosLargeInfinite({
|
||||||
initialOffset,
|
initialOffset,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
}: InfinitePhotoScrollExternalProps) {
|
}: InfinitePhotoScrollExternalProps) {
|
||||||
@ -17,11 +17,11 @@ export default function InfinitePhotoScrollPhotosLarge({
|
|||||||
wrapMoreButtonInGrid
|
wrapMoreButtonInGrid
|
||||||
>
|
>
|
||||||
{({ photos, onLastPhotoVisible, revalidatePhoto }) =>
|
{({ photos, onLastPhotoVisible, revalidatePhoto }) =>
|
||||||
<PhotosLarge {...{
|
<PhotosLarge
|
||||||
photos,
|
photos={photos}
|
||||||
onLastPhotoVisible,
|
onLastPhotoVisible={onLastPhotoVisible}
|
||||||
revalidatePhoto,
|
revalidatePhoto={revalidatePhoto}
|
||||||
}} />}
|
/>}
|
||||||
</InfinitePhotoScroll>
|
</InfinitePhotoScroll>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -195,11 +195,19 @@ export async function streamAiImageQueryAction(
|
|||||||
streamOpenAiImageQuery(imageBase64, AI_IMAGE_QUERIES[query]));
|
streamOpenAiImageQuery(imageBase64, AI_IMAGE_QUERIES[query]));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPhotosCachedAction = async (offset: number, limit: number) =>
|
export const getPhotosCachedAction = async (
|
||||||
getPhotosCachedCached({ offset, limit });
|
offset: number,
|
||||||
|
limit: number,
|
||||||
|
includeHidden?: boolean,
|
||||||
|
) =>
|
||||||
|
getPhotosCachedCached({ offset, includeHidden, limit });
|
||||||
|
|
||||||
export const getPhotosAction = async (offset: number, limit: number) =>
|
export const getPhotosAction = async (
|
||||||
getPhotos({ offset, limit });
|
offset: number,
|
||||||
|
limit: number,
|
||||||
|
includeHidden?: boolean,
|
||||||
|
) =>
|
||||||
|
getPhotos({ offset, includeHidden, limit });
|
||||||
|
|
||||||
export const queryPhotosByTitleAction = async (query: string) =>
|
export const queryPhotosByTitleAction = async (query: string) =>
|
||||||
(await getPhotos({ query, limit: 10 }))
|
(await getPhotos({ query, limit: 10 }))
|
||||||
|
|||||||
21
src/utility/useOnVisible.ts
Normal file
21
src/utility/useOnVisible.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function useOnVisible(
|
||||||
|
ref: React.RefObject<HTMLElement>,
|
||||||
|
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]);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user