Finalize transition away from param-based infinite scroll

This commit is contained in:
Sam Becker 2024-05-18 14:49:36 -05:00
parent af7424315d
commit 0f43b1977b
22 changed files with 154 additions and 229 deletions

View File

@ -1,5 +1,6 @@
'use client';
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
import InfinitePhotoScroll from '../photo/InfinitePhotoScroll';
import AdminPhotosTable from './AdminPhotosTable';
@ -12,7 +13,7 @@ export default function AdminPhotosTableInfinite({
}) {
return (
<InfinitePhotoScroll
cacheKey="AdminPhotoTable"
cacheKey={`page-${PATH_ADMIN_PHOTOS}`}
initialOffset={initialOffset}
itemsPerPage={itemsPerPage}
useCachedPhotos={false}

View File

@ -1,11 +1,7 @@
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
import { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation';
import FilmSimulationOverview from '@/simulation/FilmSimulationOverview';
import {
getPhotosFilmSimulationDataCached,
getPhotosFilmSimulationDataCachedWithPagination,
} from '@/simulation/data';
import { PaginationParams } from '@/site/pagination';
import { getPhotosFilmSimulationDataCached } from '@/simulation/data';
import { Metadata } from 'next/types';
interface FilmSimulationProps {
@ -49,16 +45,13 @@ export async function generateMetadata({
export default async function FilmSimulationPage({
params: { simulation },
searchParams,
}: FilmSimulationProps & PaginationParams) {
const {
}: FilmSimulationProps) {
const [
photos,
count,
showMorePath,
dateRange,
} = await getPhotosFilmSimulationDataCachedWithPagination({
{ count, dateRange },
] = await getPhotosFilmSimulationDataCached({
simulation,
searchParams,
limit: GRID_THUMBNAILS_TO_SHOW_MAX,
});
return (
@ -67,7 +60,6 @@ export default async function FilmSimulationPage({
photos,
count,
dateRange,
showMorePath,
}} />
);
}

View File

@ -2,11 +2,7 @@ import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
import { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation';
import FilmSimulationOverview from '@/simulation/FilmSimulationOverview';
import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal';
import {
getPhotosFilmSimulationDataCached,
getPhotosFilmSimulationDataCachedWithPagination,
} from '@/simulation/data';
import { PaginationParams } from '@/site/pagination';
import { getPhotosFilmSimulationDataCached } from '@/simulation/data';
import { Metadata } from 'next/types';
interface FilmSimulationProps {
@ -50,22 +46,19 @@ export async function generateMetadata({
export default async function Share({
params: { simulation },
searchParams,
}: FilmSimulationProps & PaginationParams) {
const {
}: FilmSimulationProps) {
const [
photos,
count,
dateRange,
showMorePath,
} = await getPhotosFilmSimulationDataCachedWithPagination({
{ count, dateRange },
] = await getPhotosFilmSimulationDataCached({
simulation,
searchParams,
limit: GRID_THUMBNAILS_TO_SHOW_MAX,
});
return <>
<FilmSimulationShareModal {...{ simulation, photos, count, dateRange }} />
<FilmSimulationOverview
{...{ simulation, photos, count, dateRange, showMorePath }}
{...{ simulation, photos, count, dateRange }}
animateOnFirstLoadOnly
/>
</>;

View File

@ -10,6 +10,7 @@ import { getPhotoSidebarData } from '@/photo/data';
import { getPhotos } from '@/photo/db';
import { cache } from 'react';
import PhotoGridPage from '@/photo/PhotoGridPage';
import { PATH_GRID } from '@/site/paths';
export const dynamic = 'force-static';
@ -38,6 +39,7 @@ export default async function GridPage() {
return (
photos.length > 0
? <PhotoGridPage
cacheKey={`page-${PATH_GRID}`}
photos={photos}
count={photosCount}
sidebar={<div className="sticky top-4 space-y-4 mt-[-4px]">

View File

@ -1,36 +1,30 @@
import { getPhotosCached, getPhotosCountCached } from '@/photo/cache';
import MoreComponentsFromSearchParams from
'@/components/MoreComponentsFromSearchParams';
import StaggeredOgPhotos from '@/photo/StaggeredOgPhotos';
import {
PaginationParams,
getPaginationFromSearchParams,
} from '@/site/pagination';
import { pathForOg } from '@/site/paths';
export default async function GridPage({ searchParams }: PaginationParams) {
const { offset, limit } = getPaginationFromSearchParams(searchParams);
INFINITE_SCROLL_INITIAL_GRID,
INFINITE_SCROLL_MULTIPLE_GRID,
} from '@/photo';
import { getPhotosCached, getPhotosCountCached } from '@/photo/cache';
import StaggeredOgPhotos from '@/photo/StaggeredOgPhotos';
import StaggeredOgPhotosInfinite from '@/photo/StaggeredOgPhotosInfinite';
export default async function GridPage() {
const [
photos,
count,
] = await Promise.all([
getPhotosCached({ limit }),
getPhotosCached({ limit: INFINITE_SCROLL_INITIAL_GRID }),
getPhotosCountCached(),
]);
const showMorePhotos = count > photos.length;
return (
<div className="space-y-3">
<div className="grid gap-3 grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<StaggeredOgPhotos photos={photos} />
</div>
{showMorePhotos &&
<MoreComponentsFromSearchParams
label="More photos"
path={pathForOg(offset + 1)}
/>}
</div>
<>
<StaggeredOgPhotos {...{ photos }} />
{count > photos.length &&
<div className="mt-3">
<StaggeredOgPhotosInfinite
initialOffset={photos.length}
itemsPerPage={INFINITE_SCROLL_MULTIPLE_GRID}
/>
</div>}
</>
);
}

View File

@ -1,12 +1,8 @@
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
import { PaginationParams } from '@/site/pagination';
import { PATH_ROOT } from '@/site/paths';
import { generateMetaForTag } from '@/tag';
import TagOverview from '@/tag/TagOverview';
import {
getPhotosTagDataCached,
getPhotosTagDataCachedWithPagination,
} from '@/tag/data';
import { getPhotosTagDataCached } from '@/tag/data';
import type { Metadata } from 'next';
import { redirect } from 'next/navigation';
@ -55,23 +51,20 @@ export async function generateMetadata({
export default async function TagPage({
params: { tag: tagFromParams },
searchParams,
}:TagProps & PaginationParams) {
}:TagProps) {
const tag = decodeURIComponent(tagFromParams);
const {
const [
photos,
count,
showMorePath,
dateRange,
} = await getPhotosTagDataCachedWithPagination({
{ count, dateRange },
] = await getPhotosTagDataCached({
tag,
searchParams,
limit: GRID_THUMBNAILS_TO_SHOW_MAX,
});
if (photos.length === 0) { redirect(PATH_ROOT); }
return (
<TagOverview {...{ tag, photos, count, dateRange, showMorePath }} />
<TagOverview {...{ tag, photos, count, dateRange }} />
);
}

View File

@ -1,12 +1,8 @@
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
import { PaginationParams } from '@/site/pagination';
import { generateMetaForTag } from '@/tag';
import TagOverview from '@/tag/TagOverview';
import TagShareModal from '@/tag/TagShareModal';
import {
getPhotosTagDataCached,
getPhotosTagDataCachedWithPagination,
} from '@/tag/data';
import { getPhotosTagDataCached } from '@/tag/data';
import type { Metadata } from 'next';
interface TagProps {
@ -52,24 +48,21 @@ export async function generateMetadata({
export default async function Share({
params: { tag: tagFromParams },
searchParams,
}: TagProps & PaginationParams) {
}: TagProps) {
const tag = decodeURIComponent(tagFromParams);
const {
const [
photos,
count,
dateRange,
showMorePath,
} = await getPhotosTagDataCachedWithPagination({
{ count, dateRange },
] = await getPhotosTagDataCached({
tag,
searchParams,
limit: GRID_THUMBNAILS_TO_SHOW_MAX,
});
return <>
<TagShareModal {...{ tag, photos, count, dateRange }} />
<TagOverview
{...{ tag, photos, count, dateRange, showMorePath }}
{...{ tag, photos, count, dateRange }}
animateOnFirstLoadOnly
/>
</>;

View File

@ -1,5 +1,5 @@
import { Photo, PhotoDateRange } from '@/photo';
import { Camera } from '.';
import { Camera, createCameraKey } from '.';
import CameraHeader from './CameraHeader';
import PhotoGridPage from '@/photo/PhotoGridPage';
@ -18,11 +18,17 @@ export default function CameraOverview({
}) {
return (
<PhotoGridPage {...{
cacheKey: `camera-${createCameraKey(camera)}`,
photos,
count,
camera,
animateOnFirstLoadOnly,
header: <CameraHeader {...{ camera, photos, count, dateRange }} />,
header: <CameraHeader {...{
camera,
photos,
count,
dateRange,
}} />,
}} />
);
}

View File

@ -1,11 +1,12 @@
'use client';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { clsx } from 'clsx/lite';
import Link from 'next/link';
import { BiError } from 'react-icons/bi';
import Spinner from '@/components/Spinner';
import { IMAGE_OG_DIMENSION } from '../image-response';
import useOnVisible from '@/utility/useOnVisible';
export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed';
@ -19,6 +20,7 @@ export default function OGTile({
onLoad,
onFail,
retryTime,
onVisible,
}: {
title: string
description: string
@ -29,7 +31,10 @@ export default function OGTile({
onFail?: () => void
riseOnHover?: boolean
retryTime?: number
onVisible?: () => void
}) {
const ref = useRef<HTMLAnchorElement>(null);
const [loadingStateInternal, setLoadingStateInternal] =
useState(loadingStateExternal ?? 'unloaded');
@ -46,8 +51,11 @@ export default function OGTile({
const { width, height, aspectRatio } = IMAGE_OG_DIMENSION;
useOnVisible(ref, onVisible);
return (
<Link
ref={ref}
href={path}
className={clsx(
'group',

View File

@ -31,7 +31,6 @@ export default function PhotoGrid({
animate?: boolean
animateOnFirstLoadOnly?: boolean
staggerOnFirstLoadOnly?: boolean
showMorePath?: string
additionalTile?: JSX.Element
small?: boolean
onLastPhotoVisible?: () => void

View File

@ -6,17 +6,19 @@ import InfinitePhotoScroll from './InfinitePhotoScroll';
import PhotoGrid from './PhotoGrid';
export default function PhotoGridInfinite({
cacheKey,
initialOffset,
camera,
animateOnFirstLoadOnly,
}: {
initialOffset: number,
camera?: Camera,
animateOnFirstLoadOnly?: boolean,
cacheKey: string
initialOffset: number
camera?: Camera
animateOnFirstLoadOnly?: boolean
}) {
return (
<InfinitePhotoScroll
cacheKey="Grid"
cacheKey={cacheKey}
initialOffset={initialOffset}
itemsPerPage={INFINITE_SCROLL_MULTIPLE_GRID}
camera={camera}

View File

@ -7,6 +7,7 @@ import { clsx } from 'clsx/lite';
import AnimateItems from '@/components/AnimateItems';
export default function PhotoGridPage({
cacheKey,
photos,
count,
camera,
@ -14,6 +15,7 @@ export default function PhotoGridPage({
header,
sidebar,
}: {
cacheKey: string
photos: Photo[]
count: number
camera?: Camera
@ -36,6 +38,7 @@ export default function PhotoGridPage({
<PhotoGrid {...{ photos, camera, animateOnFirstLoadOnly }} />
{count > photos.length &&
<PhotoGridInfinite {...{
cacheKey,
initialOffset: photos.length,
camera,
animateOnFirstLoadOnly,

View File

@ -15,6 +15,7 @@ export default function PhotoOGTile({
onLoad,
onFail,
retryTime,
onVisible,
}: {
photo: Photo
loadingState?: OGLoadingState
@ -22,6 +23,7 @@ export default function PhotoOGTile({
onFail?: () => void
riseOnHover?: boolean
retryTime?: number
onVisible?: () => void
}) {
return (
<OGTile {...{
@ -34,6 +36,7 @@ export default function PhotoOGTile({
onFail,
riseOnHover,
retryTime,
onVisible,
}}/>
);
};

View File

@ -1,5 +1,6 @@
'use client';
import { PATH_ROOT } from '@/site/paths';
import InfinitePhotoScroll from './InfinitePhotoScroll';
import PhotosLarge from './PhotosLarge';
@ -12,7 +13,7 @@ export default function PhotosLargeInfinite({
}) {
return (
<InfinitePhotoScroll
cacheKey="PhotosLarge"
cacheKey={`page-${PATH_ROOT}`}
initialOffset={initialOffset}
itemsPerPage={itemsPerPage}
wrapMoreButtonInGrid

View File

@ -11,9 +11,11 @@ type PhotoLoadingState = Record<string, OGLoadingState>;
export default function StaggeredOgPhotos({
photos,
maxConcurrency = DEFAULT_MAX_CONCURRENCY,
onLastPhotoVisible,
}: {
photos: Photo[]
maxConcurrency?: number
onLastPhotoVisible?: () => void
}) {
const [loadingState, setLoadingState] = useState(
photos.reduce((acc, photo) => ({
@ -52,13 +54,20 @@ export default function StaggeredOgPhotos({
recomputeLoadingState();
}, [recomputeLoadingState]);
return photos.map(photo =>
<PhotoOGTile
key={photo.id}
photo={photo}
loadingState={loadingState[photo.id]}
onLoad={() => recomputeLoadingState({ [photo.id]: 'loaded' })}
onFail={() => recomputeLoadingState({ [photo.id]: 'failed' })}
riseOnHover
/>);
return (
<div className="grid gap-3 grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{photos.map((photo, index) =>
<PhotoOGTile
key={photo.id}
photo={photo}
loadingState={loadingState[photo.id]}
onLoad={() => recomputeLoadingState({ [photo.id]: 'loaded' })}
onFail={() => recomputeLoadingState({ [photo.id]: 'failed' })}
onVisible={index === photos.length - 1
? onLastPhotoVisible
:undefined}
riseOnHover
/>)}
</div>
);
};

View File

@ -0,0 +1,27 @@
'use client';
import { PATH_OG } from '@/site/paths';
import InfinitePhotoScroll from './InfinitePhotoScroll';
import StaggeredOgPhotos from './StaggeredOgPhotos';
export default function StaggeredOgPhotosInfinite({
initialOffset,
itemsPerPage,
}: {
initialOffset: number
itemsPerPage: number
}) {
return (
<InfinitePhotoScroll
cacheKey={`page-${PATH_OG}`}
initialOffset={initialOffset}
itemsPerPage={itemsPerPage}
>
{({ photos, onLastPhotoVisible }) =>
<StaggeredOgPhotos
photos={photos}
onLastPhotoVisible={onLastPhotoVisible}
/>}
</InfinitePhotoScroll>
);
}

View File

@ -1,42 +1,33 @@
import { Photo, PhotoDateRange } from '@/photo';
import SiteGrid from '@/components/SiteGrid';
import AnimateItems from '@/components/AnimateItems';
import PhotoGrid from '@/photo/PhotoGrid';
import FilmSimulationHeader from './FilmSimulationHeader';
import { FilmSimulation } from '.';
import PhotoGridPage from '@/photo/PhotoGridPage';
export default function FilmSimulationOverview({
simulation,
photos,
count,
dateRange,
showMorePath,
animateOnFirstLoadOnly,
}: {
simulation: FilmSimulation,
photos: Photo[],
count: number,
dateRange?: PhotoDateRange,
showMorePath?: string,
animateOnFirstLoadOnly?: boolean,
}) {
return (
<SiteGrid
contentMain={<div className="space-y-8 mt-4">
<AnimateItems
type="bottom"
items={[
<FilmSimulationHeader
key="FilmSimulationHeader"
{...{ simulation, photos, count, dateRange }}
/>,
]}
animateOnFirstLoadOnly
/>
<PhotoGrid
{...{ photos, simulation, showMorePath, animateOnFirstLoadOnly }}
/>
</div>}
/>
<PhotoGridPage {...{
cacheKey: `simulation-${simulation}`,
photos,
count,
header: <FilmSimulationHeader {...{
simulation,
photos,
count,
dateRange,
}} />,
animateOnFirstLoadOnly,
}} />
);
}

View File

@ -2,11 +2,6 @@ import {
getPhotosCached,
getPhotosFilmSimulationMetaCached,
} from '@/photo/cache';
import {
PaginationSearchParams,
getPaginationFromSearchParams,
} from '@/site/pagination';
import { pathForFilmSimulation } from '@/site/paths';
import { FilmSimulation } from '.';
export const getPhotosFilmSimulationDataCached = ({
@ -20,32 +15,3 @@ export const getPhotosFilmSimulationDataCached = ({
getPhotosCached({ simulation, limit }),
getPhotosFilmSimulationMetaCached(simulation),
]);
export const getPhotosFilmSimulationDataCachedWithPagination = async ({
simulation,
limit: limitProp,
searchParams,
}: {
simulation: FilmSimulation,
limit?: number,
searchParams?: PaginationSearchParams,
}) => {
const { offset, limit } = getPaginationFromSearchParams(searchParams);
const [photos, { count, dateRange }] =
await getPhotosFilmSimulationDataCached({
simulation,
limit: limitProp ?? limit,
});
const showMorePath = count > photos.length
? pathForFilmSimulation(simulation, offset + 1)
: undefined;
return {
photos,
count,
dateRange,
showMorePath,
};
};

View File

@ -1,17 +0,0 @@
export type PaginationSearchParams = { next: string };
export interface PaginationParams {
searchParams?: PaginationSearchParams
}
export const getPaginationFromSearchParams = (
query?: PaginationSearchParams,
limitPerOffset = 24,
) => {
const offsetInt = parseInt(query?.next ?? '0');
const offset = (Number.isNaN(offsetInt) ? 0 : offsetInt);
return {
offset,
limit: limitPerOffset + offset * limitPerOffset,
};
};

View File

@ -252,7 +252,8 @@ export const isPathAdminConfiguration = (pathname?: string) =>
export const isPathProtected = (pathname?: string) =>
checkPathPrefix(pathname, PATH_ADMIN) ||
checkPathPrefix(pathname, pathForTag(TAG_HIDDEN));
checkPathPrefix(pathname, pathForTag(TAG_HIDDEN)) ||
pathname === PATH_OG;
export const getPathComponents = (pathname = ''): {
photoId?: string

View File

@ -1,41 +1,32 @@
import { Photo, PhotoDateRange } from '@/photo';
import SiteGrid from '@/components/SiteGrid';
import AnimateItems from '@/components/AnimateItems';
import PhotoGrid from '@/photo/PhotoGrid';
import TagHeader from './TagHeader';
import PhotoGridPage from '@/photo/PhotoGridPage';
export default function TagOverview({
tag,
photos,
count,
dateRange,
showMorePath,
animateOnFirstLoadOnly,
}: {
tag: string,
photos: Photo[],
count: number,
dateRange?: PhotoDateRange,
showMorePath?: string,
animateOnFirstLoadOnly?: boolean,
}) {
return (
<SiteGrid
contentMain={<div className="space-y-8 mt-4">
<AnimateItems
type="bottom"
items={[
<TagHeader
key="TagHeader"
{...{ tag, photos, count, dateRange }}
/>,
]}
animateOnFirstLoadOnly
/>
<PhotoGrid
{...{ photos, tag, showMorePath, animateOnFirstLoadOnly }}
/>
</div>}
/>
<PhotoGridPage {...{
cacheKey: `tag-${tag}`,
photos,
count,
header: <TagHeader {...{
tag,
photos,
count,
dateRange,
}} />,
animateOnFirstLoadOnly,
}} />
);
}

View File

@ -2,11 +2,6 @@ import {
getPhotosCachedCached,
getPhotosTagMetaCached,
} from '@/photo/cache';
import {
PaginationSearchParams,
getPaginationFromSearchParams,
} from '@/site/pagination';
import { pathForTag } from '@/site/paths';
export const getPhotosTagDataCached = ({
tag,
@ -20,31 +15,3 @@ export const getPhotosTagDataCached = ({
getPhotosTagMetaCached(tag),
]);
export const getPhotosTagDataCachedWithPagination = async ({
tag,
limit: limitProp,
searchParams,
}: {
tag: string,
limit?: number,
searchParams?: PaginationSearchParams,
}) => {
const { offset, limit } = getPaginationFromSearchParams(searchParams);
const [photos, { count, dateRange }] =
await getPhotosTagDataCached({
tag,
limit: limitProp ?? limit,
});
const showMorePath = count > photos.length
? pathForTag(tag, offset + 1)
: undefined;
return {
photos,
count,
dateRange,
showMorePath,
};
};