From e93e23f4280afccd9b514892c8dc3335645177d2 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 4 Oct 2023 13:14:19 -0500 Subject: [PATCH] Paginate camera and tag views --- src/app/(auth-state)/admin/photos/page.tsx | 14 +++--- src/app/(static)/grid/page.tsx | 25 +++++----- src/app/(static)/og/page.tsx | 13 +++-- src/app/(static)/page.tsx | 14 +++--- src/app/(static)/shot-on/[camera]/page.tsx | 44 ++++++++++++++--- .../(static)/shot-on/[camera]/share/page.tsx | 43 +++++++++++++--- src/app/(static)/t/[tag]/page.tsx | 41 +++++++++++++--- src/app/(static)/t/[tag]/share/page.tsx | 44 +++++++++++++---- src/cache/index.ts | 21 ++++++++ src/camera/CameraHeader.tsx | 5 +- src/camera/CameraOGTile.tsx | 6 ++- src/camera/CameraShareModal.tsx | 4 +- src/camera/meta.ts | 13 +++-- src/photo/PhotoGrid.tsx | 49 +++++++++++-------- src/photo/PhotoHeader.tsx | 5 +- src/photo/index.ts | 25 +++------- src/services/postgres.ts | 21 +++++++- src/site/pagination.ts | 17 +++++++ src/site/paths.ts | 14 ++++-- src/tag/TagHeader.tsx | 5 +- src/tag/TagOGTile.tsx | 4 +- src/tag/TagShareModal.tsx | 4 +- src/tag/index.ts | 19 +++++-- 23 files changed, 320 insertions(+), 130 deletions(-) create mode 100644 src/site/pagination.ts diff --git a/src/app/(auth-state)/admin/photos/page.tsx b/src/app/(auth-state)/admin/photos/page.tsx index 85914ac8..c2abb6e8 100644 --- a/src/app/(auth-state)/admin/photos/page.tsx +++ b/src/app/(auth-state)/admin/photos/page.tsx @@ -19,7 +19,7 @@ import { pathForPhoto, pathForPhotoEdit, } from '@/site/paths'; -import { getPhotosLimitForQuery, titleForPhoto } from '@/photo'; +import { titleForPhoto } from '@/photo'; import MorePhotos from '@/components/MorePhotos'; import { getBlobPhotoUrlsCached, @@ -29,17 +29,17 @@ import { } from '@/cache'; import { AiOutlineEyeInvisible } from 'react-icons/ai'; import { BiTrash } from 'react-icons/bi'; +import { + PaginationParams, + getPaginationForSearchParams, +} from '@/site/pagination'; export const runtime = 'edge'; const DEBUG_PHOTO_BLOBS = false; -export default async function AdminPage({ - searchParams, -}: { - searchParams: { next: string }; -}) { - const { offset, limit } = getPhotosLimitForQuery(searchParams.next); +export default async function AdminPage({ searchParams }: PaginationParams) { + const { offset, limit } = getPaginationForSearchParams(searchParams); const [ photos, diff --git a/src/app/(static)/grid/page.tsx b/src/app/(static)/grid/page.tsx index a01890da..6a06a20d 100644 --- a/src/app/(static)/grid/page.tsx +++ b/src/app/(static)/grid/page.tsx @@ -5,9 +5,8 @@ import { getUniqueTagsCached, } from '@/cache'; import HeaderList from '@/components/HeaderList'; -import MorePhotos from '@/components/MorePhotos'; import SiteGrid from '@/components/SiteGrid'; -import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo'; +import { generateOgImageMetaForPhotos } from '@/photo'; import PhotoCamera from '@/camera/PhotoCamera'; import PhotoGrid from '@/photo/PhotoGrid'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; @@ -17,6 +16,10 @@ import PhotoTag from '@/tag/PhotoTag'; import { Metadata } from 'next'; import { FaTag } from 'react-icons/fa'; import { IoMdCamera } from 'react-icons/io'; +import { + PaginationParams, + getPaginationForSearchParams, +} from '@/site/pagination'; export const runtime = 'edge'; @@ -25,12 +28,8 @@ export async function generateMetadata(): Promise { return generateOgImageMetaForPhotos(photos); } -export default async function GridPage({ - searchParams, -}: { - searchParams: { next: string }; -}) { - const { offset, limit } = getPhotosLimitForQuery(searchParams.next); +export default async function GridPage({ searchParams }: PaginationParams) { + const { offset, limit } = getPaginationForSearchParams(searchParams); const [ photos, @@ -44,16 +43,14 @@ export default async function GridPage({ getUniqueCamerasCached(), ]); - const showMorePhotos = count > photos.length; + const showMorePath = count > photos.length + ? pathForGrid(offset + 1) + : undefined; return ( photos.length > 0 ? - - {showMorePhotos && - } - } + contentMain={} contentSide={
{tags.length > 0 && { return generateOgImageMetaForPhotos(photos); } -export default async function HomePage({ - searchParams, -}: { - searchParams: { next: string }; -}) { - const { offset, limit } = getPhotosLimitForQuery(searchParams.next, 12); +export default async function HomePage({ searchParams }: PaginationParams) { + const { offset, limit } = getPaginationForSearchParams(searchParams, 12); const [ photos, diff --git a/src/app/(static)/shot-on/[camera]/page.tsx b/src/app/(static)/shot-on/[camera]/page.tsx index e831027d..6e0d46d0 100644 --- a/src/app/(static)/shot-on/[camera]/page.tsx +++ b/src/app/(static)/shot-on/[camera]/page.tsx @@ -1,4 +1,4 @@ -import { getPhotosCached } from '@/cache'; +import { getPhotosCached, getPhotosCountCameraCached } from '@/cache'; import SiteGrid from '@/components/SiteGrid'; import CameraHeader from '@/camera/CameraHeader'; import { getMakeModelFromCameraString } from '@/camera'; @@ -6,9 +6,15 @@ import PhotoGrid from '@/photo/PhotoGrid'; import { getUniqueCameras } from '@/services/postgres'; import { Metadata } from 'next'; import { generateMetaForCamera } from '@/camera/meta'; +import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo'; +import { pathForCamera } from '@/site/paths'; +import { + PaginationParams, + getPaginationForSearchParams, +} from '@/site/pagination'; interface CameraProps { - params: { camera: string } + params: { camera: string }, } export async function generateStaticParams() { @@ -22,14 +28,21 @@ export async function generateMetadata({ params, }: CameraProps): Promise { const camera = getMakeModelFromCameraString(params.camera); - const photos = await getPhotosCached({ camera }); + + const [ + photos, + count, + ] = await Promise.all([ + getPhotosCached({ camera, limit: GRID_THUMBNAILS_TO_SHOW_MAX }), + getPhotosCountCameraCached(camera), + ]); const { url, title, description, images, - } = generateMetaForCamera(camera, photos); + } = generateMetaForCamera(camera, photos, count); return { title, @@ -48,17 +61,32 @@ export async function generateMetadata({ }; } -export default async function CameraPage({ params }:CameraProps) { +export default async function CameraPage({ + params, + searchParams, +}: CameraProps & PaginationParams) { const camera = getMakeModelFromCameraString(params.camera); + + const { offset, limit } = getPaginationForSearchParams(searchParams); - const photos = await getPhotosCached({ camera }); + const [ + photos, + count, + ] = await Promise.all([ + getPhotosCached({ camera, limit }), + getPhotosCountCameraCached(camera), + ]); + + const showMorePath = count > photos.length + ? pathForCamera(camera, offset + 1) + : undefined; return ( - - + +
} /> ); diff --git a/src/app/(static)/shot-on/[camera]/share/page.tsx b/src/app/(static)/shot-on/[camera]/share/page.tsx index 8511e357..89034bfe 100644 --- a/src/app/(static)/shot-on/[camera]/share/page.tsx +++ b/src/app/(static)/shot-on/[camera]/share/page.tsx @@ -1,4 +1,4 @@ -import { getPhotosCached } from '@/cache'; +import { getPhotosCached, getPhotosCountCameraCached } from '@/cache'; import SiteGrid from '@/components/SiteGrid'; import { cameraFromPhoto, getMakeModelFromCameraString } from '@/camera'; import CameraHeader from '@/camera/CameraHeader'; @@ -7,6 +7,12 @@ import { generateMetaForCamera } from '@/camera/meta'; import PhotoGrid from '@/photo/PhotoGrid'; import { getUniqueCameras } from '@/services/postgres'; import { Metadata } from 'next'; +import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo'; +import { pathForCamera } from '@/site/paths'; +import { + PaginationParams, + getPaginationForSearchParams, +} from '@/site/pagination'; interface CameraProps { params: { camera: string } @@ -24,14 +30,20 @@ export async function generateMetadata({ }: CameraProps): Promise { const camera = getMakeModelFromCameraString(params.camera); - const photos = await getPhotosCached({ camera }); + const [ + photos, + count, + ] = await Promise.all([ + getPhotosCached({ camera, limit: GRID_THUMBNAILS_TO_SHOW_MAX }), + getPhotosCountCameraCached(camera), + ]); const { url, title, description, images, - } = generateMetaForCamera(camera, photos); + } = generateMetaForCamera(camera, photos, count); return { title, @@ -50,20 +62,35 @@ export async function generateMetadata({ }; } -export default async function Share({ params }: CameraProps) { +export default async function Share({ + params, + searchParams, +}: CameraProps & PaginationParams) { const cameraFromParams = getMakeModelFromCameraString(params.camera); - const photos = await getPhotosCached({ camera: cameraFromParams }); + const { offset, limit } = getPaginationForSearchParams(searchParams); + + const [ + photos, + count, + ] = await Promise.all([ + getPhotosCached({ camera: cameraFromParams, limit }), + getPhotosCountCameraCached(cameraFromParams), + ]); const camera = cameraFromPhoto(photos[0], cameraFromParams); + const showMorePath = count > photos.length + ? pathForCamera(camera, offset + 1) + : undefined; + return <> - + - - + + } /> ; diff --git a/src/app/(static)/t/[tag]/page.tsx b/src/app/(static)/t/[tag]/page.tsx index e977e19b..a70fd480 100644 --- a/src/app/(static)/t/[tag]/page.tsx +++ b/src/app/(static)/t/[tag]/page.tsx @@ -1,7 +1,13 @@ -import { getPhotosCached } from '@/cache'; +import { getPhotosCached, getPhotosCountTagCached } from '@/cache'; import SiteGrid from '@/components/SiteGrid'; +import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo'; import PhotoGrid from '@/photo/PhotoGrid'; import { getUniqueTags } from '@/services/postgres'; +import { + PaginationParams, + getPaginationForSearchParams, +} from '@/site/pagination'; +import { pathForTag } from '@/site/paths'; import { generateMetaForTag } from '@/tag'; import TagHeader from '@/tag/TagHeader'; import { Metadata } from 'next'; @@ -20,14 +26,20 @@ export async function generateStaticParams() { export async function generateMetadata({ params: { tag }, }: TagProps): Promise { - const photos = await getPhotosCached({ tag }); + const [ + photos, + count, + ] = await Promise.all([ + getPhotosCached({ tag, limit: GRID_THUMBNAILS_TO_SHOW_MAX }), + getPhotosCountTagCached(tag), + ]); const { url, title, description, images, - } = generateMetaForTag(tag, photos); + } = generateMetaForTag(tag, photos, count); return { title, @@ -46,15 +58,30 @@ export async function generateMetadata({ }; } -export default async function TagPage({ params: { tag } }:TagProps) { - const photos = await getPhotosCached({ tag }); +export default async function TagPage({ + params: { tag }, + searchParams, +}:TagProps & PaginationParams) { + const { offset, limit } = getPaginationForSearchParams(searchParams); + + const [ + photos, + count, + ] = await Promise.all([ + getPhotosCached({ tag, limit }), + getPhotosCountTagCached(tag), + ]); + + const showMorePath = count > photos.length + ? pathForTag(tag, offset + 1) + : undefined; return ( - - + + } /> ); diff --git a/src/app/(static)/t/[tag]/share/page.tsx b/src/app/(static)/t/[tag]/share/page.tsx index c81a936b..d2365615 100644 --- a/src/app/(static)/t/[tag]/share/page.tsx +++ b/src/app/(static)/t/[tag]/share/page.tsx @@ -1,7 +1,13 @@ -import { getPhotosCached } from '@/cache'; +import { getPhotosCached, getPhotosCountTagCached } from '@/cache'; import SiteGrid from '@/components/SiteGrid'; +import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo'; import PhotoGrid from '@/photo/PhotoGrid'; import { getUniqueTags } from '@/services/postgres'; +import { + PaginationParams, + getPaginationForSearchParams, +} from '@/site/pagination'; +import { pathForTag } from '@/site/paths'; import { generateMetaForTag } from '@/tag'; import TagHeader from '@/tag/TagHeader'; import TagShareModal from '@/tag/TagShareModal'; @@ -21,14 +27,20 @@ export async function generateStaticParams() { export async function generateMetadata({ params: { tag }, }: TagProps): Promise { - const photos = await getPhotosCached({ tag }); + const [ + photos, + count, + ] = await Promise.all([ + getPhotosCached({ tag, limit: GRID_THUMBNAILS_TO_SHOW_MAX }), + getPhotosCountTagCached(tag), + ]); const { url, title, description, images, - } = generateMetaForTag(tag, photos); + } = generateMetaForTag(tag, photos, count); return { title, @@ -49,17 +61,29 @@ export async function generateMetadata({ export default async function Share({ params: { tag }, -}: { - params: { tag: string } -}) { - const photos = await getPhotosCached({ tag }); + searchParams, +}: TagProps & PaginationParams) { + const { offset, limit } = getPaginationForSearchParams(searchParams); + + const [ + photos, + count, + ] = await Promise.all([ + getPhotosCached({ tag, limit }), + getPhotosCountTagCached(tag), + ]); + + const showMorePath = count > photos.length + ? pathForTag(tag, offset + 1) + : undefined; + return <> - + - - + + } /> ; diff --git a/src/cache/index.ts b/src/cache/index.ts index 1b6988b8..89074316 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -4,7 +4,9 @@ import { getPhoto, getPhotos, getPhotosCount, + getPhotosCountCamera, getPhotosCountIncludingHidden, + getPhotosCountTag, getUniqueCameras, getUniqueTags, } from '@/services/postgres'; @@ -15,7 +17,9 @@ import { AuthSession } from 'next-auth'; const TAG_PHOTOS = 'photos'; const TAG_PHOTOS_COUNT = 'photos-count'; const TAG_TAGS = 'tags'; +const TAG_TAGS_COUNT = 'tags-count'; const TAG_CAMERAS = 'cameras'; +const TAG_CAMERAS_COUNT = 'cameras-count'; const TAG_BLOB = 'blob'; // eslint-disable-next-line max-len @@ -100,6 +104,23 @@ export const getPhotosCountCached: typeof getPhotosCount = (...args) => } )(); +export const getPhotosCountTagCached: typeof getPhotosCountTag = (...args) => + unstable_cache( + () => getPhotosCountTag(...args), + [TAG_PHOTOS, TAG_TAGS_COUNT], { + tags: [TAG_PHOTOS, TAG_TAGS_COUNT], + } + )(); + +// eslint-disable-next-line max-len +export const getPhotosCountCameraCached: typeof getPhotosCountCamera = (...args) => + unstable_cache( + () => getPhotosCountCamera(...args), + [TAG_PHOTOS, TAG_CAMERAS_COUNT], { + tags: [TAG_PHOTOS, TAG_CAMERAS_COUNT], + } + )(); + export const getPhotosCountIncludingHiddenCached: typeof getPhotosCount = (...args) => unstable_cache( diff --git a/src/camera/CameraHeader.tsx b/src/camera/CameraHeader.tsx index c95df7b3..6b89ec2d 100644 --- a/src/camera/CameraHeader.tsx +++ b/src/camera/CameraHeader.tsx @@ -9,20 +9,23 @@ export default function CameraHeader({ camera: cameraProp, photos, selectedPhoto, + count, }: { camera: Camera photos: Photo[] selectedPhoto?: Photo + count?: number }) { const camera = cameraFromPhoto(photos[0], cameraProp); return ( } entityVerb="Photo" - entityDescription={descriptionForCameraPhotos(photos)} + entityDescription={descriptionForCameraPhotos(photos, undefined, count)} photos={photos} selectedPhoto={selectedPhoto} sharePath={pathForCameraShare(camera)} + count={count} /> ); } diff --git a/src/camera/CameraOGTile.tsx b/src/camera/CameraOGTile.tsx index a3c58789..10a825e1 100644 --- a/src/camera/CameraOGTile.tsx +++ b/src/camera/CameraOGTile.tsx @@ -14,6 +14,7 @@ export default function CameraOGTile({ onLoad, onFail, retryTime, + count, }: { camera: Camera photos: Photo[] @@ -22,11 +23,12 @@ export default function CameraOGTile({ onFail?: () => void riseOnHover?: boolean retryTime?: number + count?: number }) { return ( - + ); }; diff --git a/src/camera/meta.ts b/src/camera/meta.ts index 1e1c8d3c..5d4799d6 100644 --- a/src/camera/meta.ts +++ b/src/camera/meta.ts @@ -12,24 +12,27 @@ import { export const titleForCamera = ( camera: Camera, photos: Photo[], + explicitCount?: number, ) => [ 'Shot on', formatCameraText(cameraFromPhoto(photos[0], camera)), - photoQuantityText(photos), + photoQuantityText(explicitCount ?? photos.length), ].join(' '); export const descriptionForCameraPhotos = ( photos: Photo[], dateBased?: boolean, + explicitCount?: number, ) => - descriptionForPhotoSet(photos, undefined, dateBased); + descriptionForPhotoSet(photos, undefined, dateBased, explicitCount); export const generateMetaForCamera = ( camera: Camera, - photos: Photo[] + photos: Photo[], + explicitCount?: number, ) => ({ url: absolutePathForCamera(camera), - title: titleForCamera(camera, photos), - description: descriptionForCameraPhotos(photos, true), + title: titleForCamera(camera, photos, explicitCount), + description: descriptionForCameraPhotos(photos, true, explicitCount), images: absolutePathForCameraImage(camera), }); diff --git a/src/photo/PhotoGrid.tsx b/src/photo/PhotoGrid.tsx index df74603c..8c91da07 100644 --- a/src/photo/PhotoGrid.tsx +++ b/src/photo/PhotoGrid.tsx @@ -3,6 +3,7 @@ import PhotoSmall from './PhotoSmall'; import { cc } from '@/utility/css'; import AnimateItems from '@/components/AnimateItems'; import { Camera } from '@/camera'; +import MorePhotos from '@/components/MorePhotos'; export default function PhotoGrid({ photos, @@ -13,6 +14,7 @@ export default function PhotoGrid({ animate = true, animateOnFirstLoadOnly, staggerOnFirstLoadOnly = true, + showMorePath, }: { photos: Photo[] selectedPhoto?: Photo @@ -22,28 +24,33 @@ export default function PhotoGrid({ animate?: boolean animateOnFirstLoadOnly?: boolean staggerOnFirstLoadOnly?: boolean + showMorePath?: string }) { return ( - - )} - /> +
+ + )} + /> + {showMorePath && + } +
); }; diff --git a/src/photo/PhotoHeader.tsx b/src/photo/PhotoHeader.tsx index 3e5958bb..0b6d7859 100644 --- a/src/photo/PhotoHeader.tsx +++ b/src/photo/PhotoHeader.tsx @@ -9,6 +9,7 @@ export default function PhotoHeader({ photos, selectedPhoto, sharePath, + count, }: { entity: JSX.Element entityVerb: string @@ -16,6 +17,7 @@ export default function PhotoHeader({ photos: Photo[] selectedPhoto?: Photo sharePath: string + count?: number }) { const { start, end } = dateRangeForPhotos(photos); @@ -35,7 +37,8 @@ export default function PhotoHeader({ 'sm:col-span-2 md:col-span-1 lg:col-span-2', )}> {selectedPhotoIndex !== undefined - ? `${entityVerb} ${selectedPhotoIndex + 1} of ${photos.length}` + // eslint-disable-next-line max-len + ? `${entityVerb} ${selectedPhotoIndex + 1} of ${count ?? photos.length}` : entityDescription} {selectedPhotoIndex === undefined && } diff --git a/src/photo/index.ts b/src/photo/index.ts index ebd0815c..a4cabae6 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -130,18 +130,6 @@ export const getNextPhoto = (photo: Photo, photos: Photo[]) => { : undefined; }; -export const getPhotosLimitForQuery = ( - query?: string, - photosPerRequest = 24, -) => { - const offsetInt = parseInt(query ?? '0'); - const offset = (Number.isNaN(offsetInt) ? 0 : offsetInt); - return { - offset, - limit: photosPerRequest + offset * photosPerRequest, - }; -}; - export const generateOgImageMetaForPhotos = (photos: Photo[]): Metadata => { if (photos.length > 0) { return { @@ -169,23 +157,24 @@ export const translatePhotoId = (id: string) => export const titleForPhoto = (photo: Photo) => photo.title || 'Untitled'; -const labelForPhotos = (photos: Photo[]) => - photos.length === 1 ? 'Photo' : 'Photos'; +const photoLabelForCount = (count: number) => + count === 1 ? 'Photo' : 'Photos'; -export const photoQuantityText = (photos: Photo[]) => - `(${photos.length} ${labelForPhotos(photos)})`; +export const photoQuantityText = (count: number) => + `(${count} ${photoLabelForCount(count)})`; export const descriptionForPhotoSet = ( photos:Photo[], descriptor?: string, dateBased?: boolean, + explicitCount?: number, ) => dateBased ? dateRangeForPhotos(photos).description.toUpperCase() : [ - photos.length, + explicitCount ?? photos.length, descriptor, - labelForPhotos(photos), + photoLabelForCount(explicitCount ?? photos.length), ].join(' '); export const dateRangeForPhotos = (photos: Photo[]) => { diff --git a/src/services/postgres.ts b/src/services/postgres.ts index 3c33a745..2fefb5a3 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -239,6 +239,20 @@ const sqlGetPhotosCountIncludingHidden = async () => sql` SELECT COUNT(*) FROM photos `.then(({ rows }) => parseInt(rows[0].count, 10)); +const sqlGetPhotosCountTag = async (tag: string) => sql` + SELECT COUNT(*) FROM photos + WHERE ${tag}=ANY(tags) AND + hidden IS NOT TRUE +`.then(({ rows }) => parseInt(rows[0].count, 10)); + +const sqlGetPhotosCountCamera = async (camera: Camera) => sql` + SELECT COUNT(*) FROM photos + WHERE + LOWER(make)=${parameterize(camera.make)} AND + LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model)} AND + hidden IS NOT TRUE +`.then(({ rows }) => parseInt(rows[0].count, 10)); + const sqlGetUniqueTags = async () => sql` SELECT DISTINCT unnest(tags) as tag FROM photos WHERE hidden IS NOT TRUE @@ -337,7 +351,12 @@ export const getPhoto = async (id: string): Promise => { .then(photos => photos.length > 0 ? photos[0] : undefined); }; -export const getPhotosCount = () => safelyQueryPhotos(sqlGetPhotosCount); +export const getPhotosCount = () => + safelyQueryPhotos(sqlGetPhotosCount); +export const getPhotosCountTag = (tag: string) => + safelyQueryPhotos(() => sqlGetPhotosCountTag(tag)); +export const getPhotosCountCamera = (camera: Camera) => + safelyQueryPhotos(() => sqlGetPhotosCountCamera(camera)); export const getPhotosCountIncludingHidden = () => safelyQueryPhotos(sqlGetPhotosCountIncludingHidden); diff --git a/src/site/pagination.ts b/src/site/pagination.ts new file mode 100644 index 00000000..74e247e5 --- /dev/null +++ b/src/site/pagination.ts @@ -0,0 +1,17 @@ +type PaginationSearchParams = { next: string }; + +export interface PaginationParams { + searchParams?: PaginationSearchParams +} + +export const getPaginationForSearchParams = ( + query?: PaginationSearchParams, + limitPerOffset = 24, +) => { + const offsetInt = parseInt(query?.next ?? '0'); + const offset = (Number.isNaN(offsetInt) ? 0 : offsetInt); + return { + offset, + limit: limitPerOffset + offset * limitPerOffset, + }; +}; diff --git a/src/site/paths.ts b/src/site/paths.ts index 9c54fcdf..c636db01 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -72,14 +72,20 @@ export const pathForPhotoShare = ( export const pathForPhotoEdit = (photo: PhotoOrPhotoId) => `${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/edit`; -export const pathForTag = (tag: string) => - `${PREFIX_TAG}/${tag}`; +export const pathForTag = (tag: string, next?: number) => + pathWithNext( + `${PREFIX_TAG}/${tag}`, + next, + ); export const pathForTagShare = (tag: string) => `${pathForTag(tag)}/${SHARE}`; -export const pathForCamera = ({ make, model }: Camera) => - `${PREFIX_CAMERA}/${createCameraKey(make, model)}`; +export const pathForCamera = ({ make, model }: Camera, next?: number) => + pathWithNext( + `${PREFIX_CAMERA}/${createCameraKey(make, model)}`, + next, + ); export const pathForCameraShare = (camera: Camera) => `${pathForCamera(camera)}/${SHARE}`; diff --git a/src/tag/TagHeader.tsx b/src/tag/TagHeader.tsx index 1b6ac981..9f3b8d7e 100644 --- a/src/tag/TagHeader.tsx +++ b/src/tag/TagHeader.tsx @@ -8,19 +8,22 @@ export default function TagHeader({ tag, photos, selectedPhoto, + count, }: { tag: string photos: Photo[] selectedPhoto?: Photo + count?: number }) { return ( } entityVerb="Tagged" - entityDescription={descriptionForTaggedPhotos(photos)} + entityDescription={descriptionForTaggedPhotos(photos, undefined, count)} photos={photos} selectedPhoto={selectedPhoto} sharePath={pathForTagShare(tag)} + count={count} /> ); } diff --git a/src/tag/TagOGTile.tsx b/src/tag/TagOGTile.tsx index a6d9d26c..f331e899 100644 --- a/src/tag/TagOGTile.tsx +++ b/src/tag/TagOGTile.tsx @@ -13,6 +13,7 @@ export default function TagOGTile({ onLoad, onFail, retryTime, + count, }: { tag: string photos: Photo[] @@ -21,11 +22,12 @@ export default function TagOGTile({ onFail?: () => void riseOnHover?: boolean retryTime?: number + count?: number }) { return ( - + ); }; diff --git a/src/tag/index.ts b/src/tag/index.ts index 0d0d3df2..5c33844f 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -2,20 +2,29 @@ import { Photo, descriptionForPhotoSet, photoQuantityText } from '@/photo'; import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths'; import { capitalizeWords } from '@/utility/string'; -export const titleForTag = (tag: string, photos:Photo[]) => [ +export const titleForTag = ( + tag: string, + photos:Photo[], + explicitCount?: number, +) => [ capitalizeWords(tag.replaceAll('-', ' ')), - photoQuantityText(photos), + photoQuantityText(explicitCount ?? photos.length), ].join(' '); export const descriptionForTaggedPhotos = ( photos: Photo[], dateBased?: boolean, + explicitCount?: number, ) => - descriptionForPhotoSet(photos, 'tagged', dateBased); + descriptionForPhotoSet(photos, 'tagged', dateBased, explicitCount); -export const generateMetaForTag = (tag: string, photos: Photo[]) => ({ +export const generateMetaForTag = ( + tag: string, + photos: Photo[], + explicitCount?: number, +) => ({ url: absolutePathForTag(tag), - title: titleForTag(tag, photos), + title: titleForTag(tag, photos, explicitCount), description: descriptionForTaggedPhotos(photos, true), images: absolutePathForTagImage(tag), });