Merge branch 'main' into ppr-static
This commit is contained in:
commit
999d40869c
@ -6,6 +6,7 @@ import {
|
||||
PATH_ADMIN_CONFIGURATION,
|
||||
checkPathPrefix,
|
||||
isPathAdminConfiguration,
|
||||
isPathTopLevelAdmin,
|
||||
} from '@/site/paths';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { clsx } from 'clsx/lite';
|
||||
@ -16,6 +17,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { BiCog } from 'react-icons/bi';
|
||||
import { FaRegClock } from 'react-icons/fa';
|
||||
|
||||
// Updates considered recent if they occurred in past 5 minutes
|
||||
const areTimesRecent = (dates: Date[]) => dates
|
||||
.some(date => differenceInMinutes(new Date(), date) < 5);
|
||||
|
||||
@ -39,17 +41,20 @@ export default function AdminNavClient({
|
||||
.concat(adminUpdateTimes)
|
||||
, [mostRecentPhotoUpdateTime, adminUpdateTimes]);
|
||||
|
||||
const [shouldShowBanner, setShouldShowBanner] =
|
||||
const [hasRecentUpdates, setHasRecentUpdates] =
|
||||
useState(areTimesRecent(updateTimes));
|
||||
|
||||
useEffect(() => {
|
||||
// Check every 10 seconds if update times are recent
|
||||
const timeout = setTimeout(() =>
|
||||
setShouldShowBanner(areTimesRecent(updateTimes))
|
||||
, 10_000);
|
||||
return () => clearTimeout(timeout);
|
||||
// Check every 5 seconds if update times are recent
|
||||
setHasRecentUpdates(areTimesRecent(updateTimes));
|
||||
const interval = setInterval(() =>
|
||||
setHasRecentUpdates(areTimesRecent(updateTimes))
|
||||
, 5_000);
|
||||
return () => clearInterval(interval);
|
||||
}, [updateTimes]);
|
||||
|
||||
const shouldShowBanner = hasRecentUpdates && isPathTopLevelAdmin(pathname);
|
||||
|
||||
return (
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
@ -94,7 +99,7 @@ export default function AdminNavClient({
|
||||
<InfoBlock centered={false} padding="tight" color="blue">
|
||||
<div className="flex items-center gap-3">
|
||||
<FaRegClock className="flex-shrink-0" />
|
||||
Updates detected—they may take several minutes to show up
|
||||
Photo updates detected—they may take several minutes to show up
|
||||
for visitors
|
||||
</div>
|
||||
</InfoBlock>}
|
||||
|
||||
@ -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 (
|
||||
<AdminTable>
|
||||
{photos.map(photo =>
|
||||
{photos.map((photo, index) =>
|
||||
<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">
|
||||
<Link
|
||||
key={photo.id}
|
||||
@ -43,7 +53,7 @@ export default function AdminPhotoTable({
|
||||
'inline-flex items-center gap-2',
|
||||
photo.hidden && 'text-dim',
|
||||
)}>
|
||||
<span>{photo.title || 'Untitled'}</span>
|
||||
<span>{titleForPhoto(photo)}</span>
|
||||
{photo.hidden &&
|
||||
<AiOutlineEyeInvisible
|
||||
className="translate-y-[0.25px]"
|
||||
@ -91,6 +101,7 @@ export default function AdminPhotoTable({
|
||||
<FormWithConfirm
|
||||
action={deletePhotoFormAction}
|
||||
confirmText={deleteConfirmationTextForPhoto(photo)}
|
||||
onSubmit={() => revalidatePhoto?.(photo.id, true)}
|
||||
>
|
||||
<input type="hidden" name="id" value={photo.id} />
|
||||
<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 { 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 (
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
@ -59,10 +55,10 @@ export default async function AdminPhotosPage({
|
||||
</div>}
|
||||
<div className="space-y-4">
|
||||
<AdminPhotoTable photos={photos} />
|
||||
{showMorePhotos &&
|
||||
<MoreComponentsFromSearchParams
|
||||
label="More photos"
|
||||
path={pathForAdminPhotos(offset + 1)}
|
||||
{photosCount > photos.length &&
|
||||
<AdminPhotoTableInfinite
|
||||
initialOffset={INFINITE_SCROLL_INITIAL_ADMIN_PHOTOS}
|
||||
itemsPerPage={INFINITE_SCROLL_MULTIPLE_ADMIN_PHOTOS}
|
||||
/>}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { getPhotosCached } from '@/photo/cache';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import {
|
||||
INFINITE_SCROLL_INITIAL_GRID,
|
||||
@ -10,11 +9,15 @@ import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||
import { MAX_PHOTOS_TO_SHOW_OG } from '@/image-response';
|
||||
import { Metadata } from 'next/types';
|
||||
import PhotoGridSidebar from '@/photo/PhotoGridSidebar';
|
||||
import { getPhotoSidebarDataCached } from '@/photo/data';
|
||||
import InfinitePhotoScroll from '@/photo/InfinitePhotoScroll';
|
||||
import { getPhotoSidebarData } from '@/photo/data';
|
||||
import { getPhotos } from '@/services/vercel-postgres';
|
||||
import { cache } from 'react';
|
||||
import PhotoGridInfinite from '@/photo/PhotoGridInfinite';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
const getPhotosCached = cache(getPhotos);
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const photos = await getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_OG });
|
||||
return generateOgImageMetaForPhotos(photos);
|
||||
@ -29,7 +32,7 @@ export default async function GridPage() {
|
||||
simulations,
|
||||
] = await Promise.all([
|
||||
getPhotosCached({ limit: INFINITE_SCROLL_INITIAL_GRID }),
|
||||
...getPhotoSidebarDataCached(),
|
||||
...getPhotoSidebarData(),
|
||||
]);
|
||||
|
||||
return (
|
||||
@ -38,8 +41,7 @@ export default async function GridPage() {
|
||||
contentMain={<div className="space-y-0.5 sm:space-y-1">
|
||||
<PhotoGrid {...{ photos }} />
|
||||
{photosCount > photos.length &&
|
||||
<InfinitePhotoScroll
|
||||
type='grid'
|
||||
<PhotoGridInfinite
|
||||
initialOffset={INFINITE_SCROLL_INITIAL_GRID}
|
||||
itemsPerPage={INFINITE_SCROLL_MULTIPLE_GRID}
|
||||
/>}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { getPhotosCachedCached, getPhotosCountCached } from '@/photo/cache';
|
||||
import {
|
||||
INFINITE_SCROLL_INITIAL_HOME,
|
||||
INFINITE_SCROLL_MULTIPLE_HOME,
|
||||
@ -7,14 +6,18 @@ import {
|
||||
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||
import { Metadata } from 'next/types';
|
||||
import { MAX_PHOTOS_TO_SHOW_OG } from '@/image-response';
|
||||
import InfinitePhotoScroll from '../photo/InfinitePhotoScroll';
|
||||
import PhotosLarge from '@/photo/PhotosLarge';
|
||||
import { cache } from 'react';
|
||||
import { getPhotos, getPhotosCount } from '@/services/vercel-postgres';
|
||||
import PhotosLargeInfinite from '@/photo/PhotosLargeInfinite';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
const getPhotosCached = cache(getPhotos);
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
// Make homepage queries resilient to error on first time setup
|
||||
const photos = await getPhotosCachedCached({
|
||||
const photos = await getPhotosCached({
|
||||
limit: MAX_PHOTOS_TO_SHOW_OG,
|
||||
})
|
||||
.catch(() => []);
|
||||
@ -27,11 +30,11 @@ export default async function HomePage() {
|
||||
photos,
|
||||
photosCount,
|
||||
] = await Promise.all([
|
||||
getPhotosCachedCached({
|
||||
getPhotosCached({
|
||||
limit: INFINITE_SCROLL_INITIAL_HOME,
|
||||
})
|
||||
.catch(() => []),
|
||||
getPhotosCountCached()
|
||||
getPhotosCount()
|
||||
.catch(() => 0),
|
||||
]);
|
||||
|
||||
@ -40,8 +43,7 @@ export default async function HomePage() {
|
||||
? <div className="space-y-1">
|
||||
<PhotosLarge {...{ photos }} />
|
||||
{photosCount > photos.length &&
|
||||
<InfinitePhotoScroll
|
||||
type="full-frame"
|
||||
<PhotosLargeInfinite
|
||||
initialOffset={INFINITE_SCROLL_INITIAL_HOME}
|
||||
itemsPerPage={INFINITE_SCROLL_MULTIPLE_HOME}
|
||||
/>}
|
||||
|
||||
@ -24,7 +24,7 @@ export default function InfoBlock({
|
||||
case 'blue': return [
|
||||
'text-main',
|
||||
'bg-blue-50/50 border-blue-200',
|
||||
'dark:bg-blue-950/30 dark:border-blue-700/45',
|
||||
'dark:bg-blue-950/30 dark:border-blue-600/50',
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,17 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import useSwrInfinite from 'swr/infinite';
|
||||
import PhotosLarge from '@/photo/PhotosLarge';
|
||||
import {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import { getPhotosAction } from '@/photo/actions';
|
||||
import { getPhotosCachedAction, getPhotosAction } from '@/photo/actions';
|
||||
import { Photo } from '.';
|
||||
import PhotoGrid from './PhotoGrid';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
@ -20,19 +19,33 @@ export type RevalidatePhoto = (
|
||||
revalidateRemainingPhotos?: boolean,
|
||||
) => Promise<any>;
|
||||
|
||||
export default function InfinitePhotoScroll({
|
||||
type = 'full-frame',
|
||||
initialOffset,
|
||||
itemsPerPage,
|
||||
}: {
|
||||
type: 'full-frame' | 'grid'
|
||||
export type InfinitePhotoScrollExternalProps = {
|
||||
initialOffset: number
|
||||
itemsPerPage: number
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
export default function InfinitePhotoScroll({
|
||||
cacheKey,
|
||||
initialOffset,
|
||||
itemsPerPage,
|
||||
wrapMoreButtonInGrid,
|
||||
useCachedPhotos = true,
|
||||
includeHiddenPhotos,
|
||||
children,
|
||||
}: InfinitePhotoScrollExternalProps & {
|
||||
cacheKey: string
|
||||
wrapMoreButtonInGrid?: boolean
|
||||
useCachedPhotos?: boolean
|
||||
includeHiddenPhotos?: boolean
|
||||
children: (props: {
|
||||
photos: Photo[]
|
||||
onLastPhotoVisible: () => void
|
||||
revalidatePhoto?: RevalidatePhoto
|
||||
}) => ReactNode
|
||||
}) {
|
||||
const { swrTimestamp, isUserSignedIn } = useAppState();
|
||||
|
||||
const key = `${swrTimestamp}-${type}`;
|
||||
const key = `${swrTimestamp}-${cacheKey}`;
|
||||
|
||||
const keyGenerator = useCallback(
|
||||
(size: number, prev: Photo[]) => prev && prev.length === 0
|
||||
@ -40,13 +53,19 @@ export default function InfinitePhotoScroll({
|
||||
: [key, size]
|
||||
, [key]);
|
||||
|
||||
const fetcher = useCallback(([_key, size]: [string, number]) => {
|
||||
console.log('Fetching', size);
|
||||
return getPhotosAction(
|
||||
initialOffset + size * itemsPerPage,
|
||||
itemsPerPage,
|
||||
);
|
||||
}, [initialOffset, itemsPerPage]);
|
||||
const fetcher = useCallback(([_key, size]: [string, number]) =>
|
||||
useCachedPhotos
|
||||
? getPhotosCachedAction(
|
||||
initialOffset + size * itemsPerPage,
|
||||
itemsPerPage,
|
||||
includeHiddenPhotos,
|
||||
)
|
||||
: getPhotosAction(
|
||||
initialOffset + size * itemsPerPage,
|
||||
itemsPerPage,
|
||||
includeHiddenPhotos,
|
||||
)
|
||||
, [useCachedPhotos, initialOffset, itemsPerPage, includeHiddenPhotos]);
|
||||
|
||||
const { data, isLoading, isValidating, error, mutate, setSize } =
|
||||
useSwrInfinite<Photo[]>(
|
||||
@ -107,17 +126,12 @@ export default function InfinitePhotoScroll({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{type === 'full-frame'
|
||||
? <PhotosLarge {...{
|
||||
photos,
|
||||
revalidatePhoto,
|
||||
onLastPhotoVisible: advance,
|
||||
}} />
|
||||
: <PhotoGrid {...{
|
||||
photos,
|
||||
onLastPhotoVisible: advance,
|
||||
}} />}
|
||||
{!isFinished && (type === 'full-frame'
|
||||
{children({
|
||||
photos,
|
||||
onLastPhotoVisible: advance,
|
||||
revalidatePhoto,
|
||||
})}
|
||||
{!isFinished && (wrapMoreButtonInGrid
|
||||
? <SiteGrid contentMain={renderMoreButton()} />
|
||||
: renderMoreButton())}
|
||||
</div>
|
||||
|
||||
25
src/photo/PhotoGridInfinite.tsx
Normal file
25
src/photo/PhotoGridInfinite.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import InfinitePhotoScroll, {
|
||||
InfinitePhotoScrollExternalProps,
|
||||
} from './InfinitePhotoScroll';
|
||||
import PhotoGrid from './PhotoGrid';
|
||||
|
||||
export default function PhotoGridInfinite({
|
||||
initialOffset,
|
||||
itemsPerPage,
|
||||
}: InfinitePhotoScrollExternalProps) {
|
||||
return (
|
||||
<InfinitePhotoScroll
|
||||
cacheKey="Grid"
|
||||
initialOffset={initialOffset}
|
||||
itemsPerPage={itemsPerPage}
|
||||
>
|
||||
{({ photos, onLastPhotoVisible }) =>
|
||||
<PhotoGrid
|
||||
photos={photos}
|
||||
onLastPhotoVisible={onLastPhotoVisible}
|
||||
/>}
|
||||
</InfinitePhotoScroll>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<SiteGrid
|
||||
|
||||
@ -8,7 +8,8 @@ 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';
|
||||
import { useRef } from 'react';
|
||||
import useOnVisible from '@/utility/useOnVisible';
|
||||
|
||||
export default function PhotoSmall({
|
||||
photo,
|
||||
@ -31,20 +32,7 @@ export default function PhotoSmall({
|
||||
}) {
|
||||
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]);
|
||||
useOnVisible(ref, onVisible);
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
||||
@ -4,6 +4,8 @@ import Link from 'next/link';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { pathForPhoto } from '@/site/paths';
|
||||
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
|
||||
import { useRef } from 'react';
|
||||
import useOnVisible from '@/utility/useOnVisible';
|
||||
|
||||
export default function PhotoTiny({
|
||||
photo,
|
||||
@ -11,15 +13,22 @@ export default function PhotoTiny({
|
||||
selected,
|
||||
className,
|
||||
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
||||
onVisible,
|
||||
}: {
|
||||
photo: Photo
|
||||
tag?: string
|
||||
selected?: boolean
|
||||
className?: string
|
||||
prefetch?: boolean
|
||||
onVisible?: () => void
|
||||
}) {
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
useOnVisible(ref, onVisible);
|
||||
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
href={pathForPhoto(photo, tag)}
|
||||
className={clsx(
|
||||
className,
|
||||
|
||||
@ -7,14 +7,14 @@ export default function PhotosLarge({
|
||||
photos,
|
||||
animate = true,
|
||||
prefetchFirstPhotoLinks,
|
||||
revalidatePhoto,
|
||||
onLastPhotoVisible,
|
||||
revalidatePhoto,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
animate?: boolean
|
||||
prefetchFirstPhotoLinks?: boolean
|
||||
revalidatePhoto?: RevalidatePhoto
|
||||
onLastPhotoVisible?: () => void
|
||||
revalidatePhoto?: RevalidatePhoto
|
||||
}) {
|
||||
return (
|
||||
<AnimateItems
|
||||
|
||||
27
src/photo/PhotosLargeInfinite.tsx
Normal file
27
src/photo/PhotosLargeInfinite.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import InfinitePhotoScroll, {
|
||||
InfinitePhotoScrollExternalProps,
|
||||
} from './InfinitePhotoScroll';
|
||||
import PhotosLarge from './PhotosLarge';
|
||||
|
||||
export default function PhotosLargeInfinite({
|
||||
initialOffset,
|
||||
itemsPerPage,
|
||||
}: InfinitePhotoScrollExternalProps) {
|
||||
return (
|
||||
<InfinitePhotoScroll
|
||||
cacheKey="PhotosLarge"
|
||||
initialOffset={initialOffset}
|
||||
itemsPerPage={itemsPerPage}
|
||||
wrapMoreButtonInGrid
|
||||
>
|
||||
{({ photos, onLastPhotoVisible, revalidatePhoto }) =>
|
||||
<PhotosLarge
|
||||
photos={photos}
|
||||
onLastPhotoVisible={onLastPhotoVisible}
|
||||
revalidatePhoto={revalidatePhoto}
|
||||
/>}
|
||||
</InfinitePhotoScroll>
|
||||
);
|
||||
}
|
||||
@ -195,8 +195,19 @@ export async function streamAiImageQueryAction(
|
||||
streamOpenAiImageQuery(imageBase64, AI_IMAGE_QUERIES[query]));
|
||||
}
|
||||
|
||||
export const getPhotosAction = 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,
|
||||
includeHidden?: boolean,
|
||||
) =>
|
||||
getPhotos({ offset, includeHidden, limit });
|
||||
|
||||
export const queryPhotosByTitleAction = async (query: string) =>
|
||||
(await getPhotos({ query, limit: 10 }))
|
||||
|
||||
@ -4,14 +4,25 @@ import {
|
||||
getUniqueFilmSimulationsCached,
|
||||
getUniqueTagsCached,
|
||||
} from '@/photo/cache';
|
||||
import {
|
||||
getPhotosCount,
|
||||
getUniqueCameras,
|
||||
getUniqueFilmSimulations,
|
||||
getUniqueTags,
|
||||
} from '@/services/vercel-postgres';
|
||||
import { SHOW_FILM_SIMULATIONS } from '@/site/config';
|
||||
import { TAG_FAVS } from '@/tag';
|
||||
import { sortTagsObject } from '@/tag';
|
||||
|
||||
export const getPhotoSidebarData = () => [
|
||||
getPhotosCount(),
|
||||
getUniqueTags().then(sortTagsObject),
|
||||
getUniqueCameras(),
|
||||
SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulations() : [],
|
||||
] as const;
|
||||
|
||||
export const getPhotoSidebarDataCached = () => [
|
||||
getPhotosCountCached(),
|
||||
getUniqueTagsCached().then(tags =>
|
||||
tags.filter(({ tag }) => tag === TAG_FAVS).concat(
|
||||
tags.filter(({ tag }) => tag !== TAG_FAVS))),
|
||||
getUniqueTagsCached().then(sortTagsObject),
|
||||
getUniqueCamerasCached(),
|
||||
SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
|
||||
] as const;
|
||||
|
||||
@ -5,7 +5,7 @@ import { makeUrlAbsolute, shortenUrl } from '@/utility/url';
|
||||
// HARD-CODED GLOBAL CONFIGURATION
|
||||
|
||||
export const SHOULD_PREFETCH_ALL_LINKS: boolean | undefined = undefined;
|
||||
export const SHOULD_DEBUG_SQL = false;
|
||||
export const SHOULD_DEBUG_SQL = true;
|
||||
|
||||
// META / DOMAINS
|
||||
|
||||
|
||||
@ -241,6 +241,9 @@ export const isPathSignIn = (pathname?: string) =>
|
||||
export const isPathAdmin = (pathname?: string) =>
|
||||
checkPathPrefix(pathname, PATH_ADMIN);
|
||||
|
||||
export const isPathTopLevelAdmin = (pathname?: string) =>
|
||||
PATHS_ADMIN.some(path => path === pathname);
|
||||
|
||||
export const isPathAdminConfiguration = (pathname?: string) =>
|
||||
checkPathPrefix(pathname, PATH_ADMIN_CONFIGURATION);
|
||||
|
||||
|
||||
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