From 80823c8d143d75ec2a1d91a7fea47e5b144e0fe7 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 4 Oct 2023 19:01:17 -0500 Subject: [PATCH] Consolidate camera/tag pagination/date handling --- .../shot-on/[camera]/[photoId]/layout.tsx | 21 ++++-- src/app/(static)/shot-on/[camera]/page.tsx | 50 +++++-------- .../(static)/shot-on/[camera]/share/page.tsx | 53 ++++++------- src/app/(static)/t/[tag]/[photoId]/layout.tsx | 27 ++++--- .../(static)/t/[tag]/[photoId]/share/page.tsx | 10 ++- src/app/(static)/t/[tag]/page.tsx | 49 +++++------- src/app/(static)/t/[tag]/share/page.tsx | 53 ++++++------- src/cache/index.ts | 75 +++++++++++++------ src/camera/CameraHeader.tsx | 8 +- src/camera/CameraOGTile.tsx | 6 +- src/camera/CameraOverview.tsx | 42 +++++++++++ src/camera/CameraShareModal.tsx | 8 +- src/camera/data.ts | 53 +++++++++++++ src/camera/meta.ts | 20 ++++- src/components/AnimateItems.tsx | 11 ++- src/components/Nav.tsx | 3 +- src/photo/PhotoDetailPage.tsx | 9 ++- src/photo/PhotoHeader.tsx | 6 +- src/photo/index.ts | 23 ++++-- src/services/postgres.ts | 34 +++++++-- src/site/pagination.ts | 2 +- src/tag/TagOverview.tsx | 41 ++++++++++ src/tag/data.ts | 52 +++++++++++++ src/tag/index.ts | 20 ++++- 24 files changed, 469 insertions(+), 207 deletions(-) create mode 100644 src/camera/CameraOverview.tsx create mode 100644 src/camera/data.ts create mode 100644 src/tag/TagOverview.tsx create mode 100644 src/tag/data.ts diff --git a/src/app/(static)/shot-on/[camera]/[photoId]/layout.tsx b/src/app/(static)/shot-on/[camera]/[photoId]/layout.tsx index f1912648..71f63ee0 100644 --- a/src/app/(static)/shot-on/[camera]/[photoId]/layout.tsx +++ b/src/app/(static)/shot-on/[camera]/[photoId]/layout.tsx @@ -10,9 +10,14 @@ import { absolutePathForPhotoImage, } from '@/site/paths'; import PhotoDetailPage from '@/photo/PhotoDetailPage'; -import { getPhotoCached, getPhotosCached } from '@/cache'; -import { getPhotos, getUniqueCameras } from '@/services/postgres'; +import { getPhotoCached } from '@/cache'; +import { + getPhotos, + getUniqueCameras, +} from '@/services/postgres'; import { cameraFromPhoto } from '@/camera'; +import { getPhotosCameraDataCached } from '@/camera/data'; +import { ReactNode } from 'react'; interface PhotoCameraProps { params: { photoId: string, camera: string } @@ -69,19 +74,21 @@ export async function generateMetadata({ export default async function PhotoCameraPage({ params: { photoId, camera: cameraProp }, children, -}: PhotoCameraProps & { - children: React.ReactNode -}) { +}: PhotoCameraProps & { children: ReactNode }) { const photo = await getPhotoCached(photoId); if (!photo) { redirect(PATH_ROOT); } const camera = cameraFromPhoto(photo, cameraProp); - const photos = await getPhotosCached({ camera }); + const [ + photos, + count, + dateRange, + ] = await getPhotosCameraDataCached({ camera }); return <> {children} - + ; } diff --git a/src/app/(static)/shot-on/[camera]/page.tsx b/src/app/(static)/shot-on/[camera]/page.tsx index 8376b1c7..b5c3bb6a 100644 --- a/src/app/(static)/shot-on/[camera]/page.tsx +++ b/src/app/(static)/shot-on/[camera]/page.tsx @@ -1,16 +1,13 @@ -import { getPhotosCached, getPhotosCountCameraCached } from '@/cache'; -import SiteGrid from '@/components/SiteGrid'; -import CameraHeader from '@/camera/CameraHeader'; import { getMakeModelFromCameraString } from '@/camera'; -import PhotoGrid from '@/photo/PhotoGrid'; import { Metadata } from 'next'; import { generateMetaForCamera } from '@/camera/meta'; import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo'; -import { pathForCamera } from '@/site/paths'; +import { PaginationParams } from '@/site/pagination'; import { - PaginationParams, - getPaginationForSearchParams, -} from '@/site/pagination'; + getPhotosCameraDataCached, + getPhotosCameraDataCachedWithPagination, +} from '@/camera/data'; +import CameraOverview from '@/camera/CameraOverview'; export const runtime = 'edge'; @@ -26,17 +23,18 @@ export async function generateMetadata({ const [ photos, count, - ] = await Promise.all([ - getPhotosCached({ camera, limit: GRID_THUMBNAILS_TO_SHOW_MAX }), - getPhotosCountCameraCached(camera), - ]); + dateRange, + ] = await getPhotosCameraDataCached({ + camera, + limit: GRID_THUMBNAILS_TO_SHOW_MAX, + }); const { url, title, description, images, - } = generateMetaForCamera(camera, photos, count); + } = generateMetaForCamera(camera, photos, count, dateRange); return { title, @@ -61,27 +59,17 @@ export default async function CameraPage({ }: CameraProps & PaginationParams) { const camera = getMakeModelFromCameraString(params.camera); - const { offset, limit } = getPaginationForSearchParams(searchParams); - - const [ + const { photos, count, - ] = await Promise.all([ - getPhotosCached({ camera, limit }), - getPhotosCountCameraCached(camera), - ]); - - const showMorePath = count > photos.length - ? pathForCamera(camera, offset + 1) - : undefined; + showMorePath, + dateRange, + } = await getPhotosCameraDataCachedWithPagination({ + camera, + searchParams, + }); return ( - - - - } - /> + ); } diff --git a/src/app/(static)/shot-on/[camera]/share/page.tsx b/src/app/(static)/shot-on/[camera]/share/page.tsx index 48a8a74a..2b2ca338 100644 --- a/src/app/(static)/shot-on/[camera]/share/page.tsx +++ b/src/app/(static)/shot-on/[camera]/share/page.tsx @@ -1,17 +1,14 @@ -import { getPhotosCached, getPhotosCountCameraCached } from '@/cache'; -import SiteGrid from '@/components/SiteGrid'; import { cameraFromPhoto, getMakeModelFromCameraString } from '@/camera'; -import CameraHeader from '@/camera/CameraHeader'; import CameraShareModal from '@/camera/CameraShareModal'; import { generateMetaForCamera } from '@/camera/meta'; -import PhotoGrid from '@/photo/PhotoGrid'; import { Metadata } from 'next'; import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo'; -import { pathForCamera } from '@/site/paths'; +import { PaginationParams } from '@/site/pagination'; import { - PaginationParams, - getPaginationForSearchParams, -} from '@/site/pagination'; + getPhotosCameraDataCached, + getPhotosCameraDataCachedWithPagination, +} from '@/camera/data'; +import CameraOverview from '@/camera/CameraOverview'; export const runtime = 'edge'; @@ -27,17 +24,18 @@ export async function generateMetadata({ const [ photos, count, - ] = await Promise.all([ - getPhotosCached({ camera, limit: GRID_THUMBNAILS_TO_SHOW_MAX }), - getPhotosCountCameraCached(camera), - ]); + dateRange, + ] = await getPhotosCameraDataCached({ + camera, + limit: GRID_THUMBNAILS_TO_SHOW_MAX, + }); const { url, title, description, images, - } = generateMetaForCamera(camera, photos, count); + } = generateMetaForCamera(camera, photos, count, dateRange); return { title, @@ -62,30 +60,23 @@ export default async function Share({ }: CameraProps & PaginationParams) { const cameraFromParams = getMakeModelFromCameraString(params.camera); - const { offset, limit } = getPaginationForSearchParams(searchParams); - - const [ + const { photos, count, - ] = await Promise.all([ - getPhotosCached({ camera: cameraFromParams, limit }), - getPhotosCountCameraCached(cameraFromParams), - ]); + dateRange, + showMorePath, + } = await getPhotosCameraDataCachedWithPagination({ + camera: cameraFromParams, + searchParams, + }); 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]/[photoId]/layout.tsx b/src/app/(static)/t/[tag]/[photoId]/layout.tsx index 1aca6497..81be6968 100644 --- a/src/app/(static)/t/[tag]/[photoId]/layout.tsx +++ b/src/app/(static)/t/[tag]/[photoId]/layout.tsx @@ -10,11 +10,17 @@ import { absolutePathForPhotoImage, } from '@/site/paths'; import PhotoDetailPage from '@/photo/PhotoDetailPage'; -import { getPhotoCached, getPhotosCached } from '@/cache'; +import { getPhotoCached } from '@/cache'; import { getPhotos, getUniqueTags } from '@/services/postgres'; +import { getPhotosTagDataCached } from '@/tag/data'; +import { ReactNode } from 'react'; + +interface PhotoTagProps { + params: { photoId: string, tag: string } +} export async function generateStaticParams() { - const params: { params: { photoId: string, tag: string }}[] = []; + const params: PhotoTagProps[] = []; const tags = await getUniqueTags(); tags.forEach(async tag => { @@ -29,9 +35,7 @@ export async function generateStaticParams() { export async function generateMetadata({ params: { photoId, tag }, -}: { - params: { photoId: string, tag: string } -}): Promise { +}: PhotoTagProps): Promise { const photo = await getPhotoCached(photoId); if (!photo) { return {}; } @@ -62,18 +66,19 @@ export async function generateMetadata({ export default async function PhotoTagPage({ params: { photoId, tag }, children, -}: { - params: { photoId: string, tag: string } - children: React.ReactNode -}) { +}: PhotoTagProps & { children: ReactNode }) { const photo = await getPhotoCached(photoId); if (!photo) { redirect(PATH_ROOT); } - const photos = await getPhotosCached({ tag }); + const [ + photos, + count, + dateRange, + ] = await getPhotosTagDataCached({ tag }); return <> {children} - + ; } diff --git a/src/app/(static)/t/[tag]/[photoId]/share/page.tsx b/src/app/(static)/t/[tag]/[photoId]/share/page.tsx index f120d70f..33ac07d1 100644 --- a/src/app/(static)/t/[tag]/[photoId]/share/page.tsx +++ b/src/app/(static)/t/[tag]/[photoId]/share/page.tsx @@ -4,8 +4,12 @@ import { getPhotos, getUniqueTags } from '@/services/postgres'; import { PATH_ROOT } from '@/site/paths'; import { redirect } from 'next/navigation'; +interface PhotoTagProps { + params: { photoId: string, tag: string } +} + export async function generateStaticParams() { - const params: { params: { photoId: string, tag: string }}[] = []; + const params: PhotoTagProps[] = []; const tags = await getUniqueTags(); tags.forEach(async tag => { @@ -20,9 +24,7 @@ export async function generateStaticParams() { export default async function Share({ params: { photoId, tag }, -}: { - params: { photoId: string, tag: string } -}) { +}: PhotoTagProps) { const photo = await getPhotoCached(photoId); if (!photo) { return redirect(PATH_ROOT); } diff --git a/src/app/(static)/t/[tag]/page.tsx b/src/app/(static)/t/[tag]/page.tsx index 72d1651c..ef37b920 100644 --- a/src/app/(static)/t/[tag]/page.tsx +++ b/src/app/(static)/t/[tag]/page.tsx @@ -1,14 +1,11 @@ -import { getPhotosCached, getPhotosCountTagCached } from '@/cache'; -import SiteGrid from '@/components/SiteGrid'; import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo'; -import PhotoGrid from '@/photo/PhotoGrid'; -import { - PaginationParams, - getPaginationForSearchParams, -} from '@/site/pagination'; -import { pathForTag } from '@/site/paths'; +import { PaginationParams } from '@/site/pagination'; import { generateMetaForTag } from '@/tag'; -import TagHeader from '@/tag/TagHeader'; +import TagOverview from '@/tag/TagOverview'; +import { + getPhotosTagDataCached, + getPhotosTagDataCachedWithPagination, +} from '@/tag/data'; import { Metadata } from 'next'; export const runtime = 'edge'; @@ -23,10 +20,10 @@ export async function generateMetadata({ const [ photos, count, - ] = await Promise.all([ - getPhotosCached({ tag, limit: GRID_THUMBNAILS_TO_SHOW_MAX }), - getPhotosCountTagCached(tag), - ]); + ] = await getPhotosTagDataCached({ + tag, + limit: GRID_THUMBNAILS_TO_SHOW_MAX, + }); const { url, @@ -56,27 +53,17 @@ export default async function TagPage({ params: { tag }, searchParams, }:TagProps & PaginationParams) { - const { offset, limit } = getPaginationForSearchParams(searchParams); - - const [ + const { photos, count, - ] = await Promise.all([ - getPhotosCached({ tag, limit }), - getPhotosCountTagCached(tag), - ]); - - const showMorePath = count > photos.length - ? pathForTag(tag, offset + 1) - : undefined; + showMorePath, + dateRange, + } = await getPhotosTagDataCachedWithPagination({ + tag, + searchParams, + }); return ( - - - - } - /> + ); } diff --git a/src/app/(static)/t/[tag]/share/page.tsx b/src/app/(static)/t/[tag]/share/page.tsx index f113bd88..03b77164 100644 --- a/src/app/(static)/t/[tag]/share/page.tsx +++ b/src/app/(static)/t/[tag]/share/page.tsx @@ -1,15 +1,12 @@ -import { getPhotosCached, getPhotosCountTagCached } from '@/cache'; -import SiteGrid from '@/components/SiteGrid'; import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo'; -import PhotoGrid from '@/photo/PhotoGrid'; -import { - PaginationParams, - getPaginationForSearchParams, -} from '@/site/pagination'; -import { pathForTag } from '@/site/paths'; +import { PaginationParams } from '@/site/pagination'; import { generateMetaForTag } from '@/tag'; -import TagHeader from '@/tag/TagHeader'; +import TagOverview from '@/tag/TagOverview'; import TagShareModal from '@/tag/TagShareModal'; +import { + getPhotosTagDataCached, + getPhotosTagDataCachedWithPagination, +} from '@/tag/data'; import { Metadata } from 'next'; export const runtime = 'edge'; @@ -24,17 +21,18 @@ export async function generateMetadata({ const [ photos, count, - ] = await Promise.all([ - getPhotosCached({ tag, limit: GRID_THUMBNAILS_TO_SHOW_MAX }), - getPhotosCountTagCached(tag), - ]); + dateRange, + ] = await getPhotosTagDataCached({ + tag, + limit: GRID_THUMBNAILS_TO_SHOW_MAX, + }); const { url, title, description, images, - } = generateMetaForTag(tag, photos, count); + } = generateMetaForTag(tag, photos, count, dateRange); return { title, @@ -57,28 +55,21 @@ export default async function Share({ params: { tag }, searchParams, }: TagProps & PaginationParams) { - const { offset, limit } = getPaginationForSearchParams(searchParams); - - const [ + const { photos, count, - ] = await Promise.all([ - getPhotosCached({ tag, limit }), - getPhotosCountTagCached(tag), - ]); - - const showMorePath = count > photos.length - ? pathForTag(tag, offset + 1) - : undefined; + dateRange, + showMorePath, + } = await getPhotosTagDataCachedWithPagination({ + tag, + searchParams, + }); return <> - - - - } + ; } diff --git a/src/cache/index.ts b/src/cache/index.ts index 99c650a5..a3ce19e5 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -4,22 +4,25 @@ import { getPhoto, getPhotos, getPhotosCount, - getPhotosCountCamera, + getPhotosCameraCount, getPhotosCountIncludingHidden, - getPhotosCountTag, + getPhotosTagCount, getUniqueCameras, getUniqueTags, + getPhotosTagDateRange, + getPhotosCameraDateRange, } from '@/services/postgres'; import { parseCachedPhotosDates, parseCachedPhotoDates } from '@/photo'; import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob'; import { AuthSession } from 'next-auth'; import { Camera, createCameraKey } from '@/camera'; -const TAG_PHOTOS = 'photos'; -const TAG_PHOTOS_COUNT = 'photos-count'; -const TAG_TAGS = 'tags'; -const TAG_CAMERAS = 'cameras'; -const TAG_BLOB = 'blob'; +const TAG_PHOTOS = 'photos'; +const TAG_PHOTOS_COUNT = `${TAG_PHOTOS}-count`; +const TAG_PHOTOS_DATE_RANGE = `${TAG_PHOTOS}-date-range`;; +const TAG_TAGS = 'tags'; +const TAG_CAMERAS = 'cameras'; +const TAG_BLOB = 'blob'; // eslint-disable-next-line max-len const getPhotosCacheTagForKey = ( @@ -69,6 +72,12 @@ const getPhotoTagCountTag = (tag: string) => const getPhotoCameraCountTag = ({ make, model }: Camera) => `${TAG_PHOTOS_COUNT}-${TAG_CAMERAS}-${createCameraKey(make, model)}`; +const getPhotoTagDateRangeTag = (tag: string) => + `${TAG_PHOTOS_DATE_RANGE}-${TAG_TAGS}-${tag}`; + +const getPhotoCameraDateRangeTag = ({ make, model }: Camera) => + `${TAG_PHOTOS_DATE_RANGE}-${TAG_CAMERAS}-${createCameraKey(make, model)}`; + export const revalidatePhotosTag = () => revalidateTag(TAG_PHOTOS); @@ -109,23 +118,6 @@ export const getPhotosCountCached: typeof getPhotosCount = (...args) => } )(); -export const getPhotosCountTagCached: typeof getPhotosCountTag = (...args) => - unstable_cache( - () => getPhotosCountTag(...args), - [TAG_PHOTOS, getPhotoTagCountTag(...args)], { - tags: [TAG_PHOTOS, getPhotoTagCountTag(...args)], - } - )(); - -// eslint-disable-next-line max-len -export const getPhotosCountCameraCached: typeof getPhotosCountCamera = (...args) => - unstable_cache( - () => getPhotosCountCamera(...args), - [TAG_PHOTOS, getPhotoCameraCountTag(...args)], { - tags: [TAG_PHOTOS, getPhotoCameraCountTag(...args)], - } - )(); - export const getPhotosCountIncludingHiddenCached: typeof getPhotosCount = (...args) => unstable_cache( @@ -135,6 +127,41 @@ export const getPhotosCountIncludingHiddenCached: typeof getPhotosCount = } )(); +export const getPhotosTagCountCached: typeof getPhotosTagCount = (...args) => + unstable_cache( + () => getPhotosTagCount(...args), + [TAG_PHOTOS, getPhotoTagCountTag(...args)], { + tags: [TAG_PHOTOS, getPhotoTagCountTag(...args)], + } + )(); + +// eslint-disable-next-line max-len +export const getPhotosCameraCountCached: typeof getPhotosCameraCount = (...args) => + unstable_cache( + () => getPhotosCameraCount(...args), + [TAG_PHOTOS, getPhotoCameraCountTag(...args)], { + tags: [TAG_PHOTOS, getPhotoCameraCountTag(...args)], + } + )(); + +// eslint-disable-next-line max-len +export const getPhotosTagDateRangeCached: typeof getPhotosTagDateRange = (...args) => + unstable_cache( + () => getPhotosTagDateRange(...args), + [TAG_PHOTOS, getPhotoTagDateRangeTag(...args)], { + tags: [TAG_PHOTOS, getPhotoTagDateRangeTag(...args)], + } + )(); + +// eslint-disable-next-line max-len +export const getPhotosCameraDateRangeCached: typeof getPhotosCameraDateRange = (...args) => + unstable_cache( + () => getPhotosCameraDateRange(...args), + [TAG_PHOTOS, getPhotoCameraDateRangeTag(...args)], { + tags: [TAG_PHOTOS, getPhotoCameraDateRangeTag(...args)], + } + )(); + export const getPhotoCached: typeof getPhoto = (...args) => unstable_cache( () => getPhoto(...args), diff --git a/src/camera/CameraHeader.tsx b/src/camera/CameraHeader.tsx index 6b89ec2d..032216f5 100644 --- a/src/camera/CameraHeader.tsx +++ b/src/camera/CameraHeader.tsx @@ -1,4 +1,4 @@ -import { Photo } from '@/photo'; +import { Photo, PhotoDateRange } from '@/photo'; import { pathForCameraShare } from '@/site/paths'; import PhotoHeader from '@/photo/PhotoHeader'; import { Camera, cameraFromPhoto } from '.'; @@ -10,22 +10,26 @@ export default function CameraHeader({ photos, selectedPhoto, count, + dateRange, }: { camera: Camera photos: Photo[] selectedPhoto?: Photo count?: number + dateRange?: PhotoDateRange }) { const camera = cameraFromPhoto(photos[0], cameraProp); return ( } entityVerb="Photo" - entityDescription={descriptionForCameraPhotos(photos, undefined, count)} + entityDescription={ + descriptionForCameraPhotos(photos, undefined, count, dateRange)} photos={photos} selectedPhoto={selectedPhoto} sharePath={pathForCameraShare(camera)} count={count} + dateRange={dateRange} /> ); } diff --git a/src/camera/CameraOGTile.tsx b/src/camera/CameraOGTile.tsx index 10a825e1..225758fa 100644 --- a/src/camera/CameraOGTile.tsx +++ b/src/camera/CameraOGTile.tsx @@ -1,4 +1,4 @@ -import { Photo } from '@/photo'; +import { Photo, PhotoDateRange } from '@/photo'; import { absolutePathForCameraImage, pathForCamera } from '@/site/paths'; import OGTile from '@/components/OGTile'; import { Camera } from '.'; @@ -15,6 +15,7 @@ export default function CameraOGTile({ onFail, retryTime, count, + dateRange, }: { camera: Camera photos: Photo[] @@ -24,11 +25,12 @@ export default function CameraOGTile({ riseOnHover?: boolean retryTime?: number count?: number + dateRange?: PhotoDateRange }) { return ( + , + ]} + animateOnFirstLoadOnly + /> + + } + /> + ); +} diff --git a/src/camera/CameraShareModal.tsx b/src/camera/CameraShareModal.tsx index d6736d87..1dd33484 100644 --- a/src/camera/CameraShareModal.tsx +++ b/src/camera/CameraShareModal.tsx @@ -1,5 +1,5 @@ import { absolutePathForCamera, pathForCamera } from '@/site/paths'; -import { Photo } from '../photo'; +import { Photo, PhotoDateRange } from '../photo'; import ShareModal from '@/components/ShareModal'; import CameraOGTile from './CameraOGTile'; import { Camera } from '.'; @@ -8,10 +8,12 @@ export default function CameraShareModal({ camera, photos, count, + dateRange, }: { camera: Camera photos: Photo[] - count?: number + count: number + dateRange: PhotoDateRange, }) { return ( - + ); }; diff --git a/src/camera/data.ts b/src/camera/data.ts new file mode 100644 index 00000000..07c795c2 --- /dev/null +++ b/src/camera/data.ts @@ -0,0 +1,53 @@ +import { + PaginationSearchParams, + getPaginationForSearchParams, +} from '@/site/pagination'; +import { Camera } from '.'; +import { + getPhotosCached, + getPhotosCameraCountCached, + getPhotosCameraDateRangeCached, +} from '@/cache'; +import { pathForCamera } from '@/site/paths'; + +export const getPhotosCameraDataCached = ({ + camera, + limit, +}: { + camera: Camera, + limit?: number, +}) => + Promise.all([ + getPhotosCached({ camera, limit }), + getPhotosCameraCountCached(camera), + getPhotosCameraDateRangeCached(camera), + ]); + +export const getPhotosCameraDataCachedWithPagination = async ({ + camera, + limit: limitProp, + searchParams, +}: { + camera: Camera, + limit?: number, + searchParams?: PaginationSearchParams, +}) => { + const { offset, limit } = getPaginationForSearchParams(searchParams); + + const [photos, count, dateRange] = + await getPhotosCameraDataCached({ + camera, + limit: limitProp ?? limit, + }); + + const showMorePath = count > photos.length + ? pathForCamera(camera, offset + 1) + : undefined; + + return { + photos, + count, + dateRange, + showMorePath, + }; +}; diff --git a/src/camera/meta.ts b/src/camera/meta.ts index 5d4799d6..35c0c2ca 100644 --- a/src/camera/meta.ts +++ b/src/camera/meta.ts @@ -1,4 +1,9 @@ -import { Photo, descriptionForPhotoSet, photoQuantityText } from '@/photo'; +import { + Photo, + PhotoDateRange, + descriptionForPhotoSet, + photoQuantityText, +} from '@/photo'; import { Camera, cameraFromPhoto, formatCameraText } from '.'; import { absolutePathForCamera, @@ -23,16 +28,25 @@ export const descriptionForCameraPhotos = ( photos: Photo[], dateBased?: boolean, explicitCount?: number, + explicitDateRange?: PhotoDateRange, ) => - descriptionForPhotoSet(photos, undefined, dateBased, explicitCount); + descriptionForPhotoSet( + photos, + undefined, + dateBased, + explicitCount, + explicitDateRange, + ); export const generateMetaForCamera = ( camera: Camera, photos: Photo[], explicitCount?: number, + explicitDateRange?: PhotoDateRange, ) => ({ url: absolutePathForCamera(camera), title: titleForCamera(camera, photos, explicitCount), - description: descriptionForCameraPhotos(photos, true, explicitCount), + description: + descriptionForCameraPhotos(photos, true, explicitCount, explicitDateRange), images: absolutePathForCameraImage(camera), }); diff --git a/src/components/AnimateItems.tsx b/src/components/AnimateItems.tsx index c650de48..a2873e0d 100644 --- a/src/components/AnimateItems.tsx +++ b/src/components/AnimateItems.tsx @@ -1,10 +1,10 @@ 'use client'; import { useRef } from 'react'; -import { motion } from 'framer-motion'; +import { Variant, motion } from 'framer-motion'; import { useAppState } from '@/state'; -export type AnimationType = 'none' | 'scale' | 'left' | 'right'; +export type AnimationType = 'none' | 'scale' | 'left' | 'right' | 'bottom'; export interface AnimationConfig { type?: AnimationType @@ -58,7 +58,7 @@ function AnimateItems({ ? (nextPhotoAnimationInitial.current?.duration ?? duration) : duration; - const getInitialVariant = () => { + const getInitialVariant = (): Variant => { switch (typeResolved) { case 'left': return { opacity: 0, @@ -68,6 +68,10 @@ function AnimateItems({ opacity: 0, transform: `translateX(${-distanceOffset}px)`, }; + case 'bottom': return { + opacity: 0, + transform: `translateY(${distanceOffset}px)`, + }; default: return { opacity: 0, transform: `translateY(${distanceOffset}px) scale(${scaleOffset})`, @@ -98,7 +102,6 @@ function AnimateItems({ @@ -40,10 +44,11 @@ export default function PhotoDetailPage({ className="mt-4 mb-8" contentMain={ } />} photo.id === selectedPhoto.id) diff --git a/src/photo/index.ts b/src/photo/index.ts index a4cabae6..c310b148 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -60,7 +60,6 @@ export interface Photo extends PhotoDb { exposureTimeFormatted?: string exposureCompensationFormatted?: string takenAtNaiveFormatted: string - takenAtNaiveFormattedShort: string } export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { @@ -84,8 +83,6 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { formatExposureCompensation(photoDb.exposureCompensation), takenAtNaiveFormatted: formatDateFromPostgresString(photoDb.takenAtNaive), - takenAtNaiveFormattedShort: - formatDateFromPostgresString(photoDb.takenAtNaive, true), }; }; @@ -163,23 +160,35 @@ const photoLabelForCount = (count: number) => export const photoQuantityText = (count: number) => `(${count} ${photoLabelForCount(count)})`; +export type PhotoDateRange = { start: string, end: string }; + export const descriptionForPhotoSet = ( photos:Photo[], descriptor?: string, dateBased?: boolean, explicitCount?: number, + explicitDateRange?: PhotoDateRange, ) => dateBased - ? dateRangeForPhotos(photos).description.toUpperCase() + ? dateRangeForPhotos(photos, explicitDateRange).description.toUpperCase() : [ explicitCount ?? photos.length, descriptor, photoLabelForCount(explicitCount ?? photos.length), ].join(' '); -export const dateRangeForPhotos = (photos: Photo[]) => { - const start = photos[0].takenAtNaiveFormattedShort; - const end = photos[photos.length - 1].takenAtNaiveFormattedShort; +export const dateRangeForPhotos = ( + photos: Photo[], + explicitDateRange?: PhotoDateRange, +) => { + const start = formatDateFromPostgresString( + explicitDateRange?.start ?? photos[0].takenAtNaive, + true, + ); + const end = formatDateFromPostgresString( + explicitDateRange?.end ?? photos[photos.length - 1].takenAtNaive, + true + ); const description = start === end ? start : `${start}–${end}`; diff --git a/src/services/postgres.ts b/src/services/postgres.ts index 2fefb5a3..3ed1a1fb 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -5,6 +5,7 @@ import { translatePhotoId, parsePhotoFromDb, Photo, + PhotoDateRange, } from '@/photo'; import { Camera, createCameraKey } from '@/camera'; import { parameterize } from '@/utility/string'; @@ -239,13 +240,13 @@ const sqlGetPhotosCountIncludingHidden = async () => sql` SELECT COUNT(*) FROM photos `.then(({ rows }) => parseInt(rows[0].count, 10)); -const sqlGetPhotosCountTag = async (tag: string) => sql` +const sqlGetPhotosTagCount = 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` +const sqlGetPhotosCameraCount = async (camera: Camera) => sql` SELECT COUNT(*) FROM photos WHERE LOWER(make)=${parameterize(camera.make)} AND @@ -253,6 +254,22 @@ const sqlGetPhotosCountCamera = async (camera: Camera) => sql` hidden IS NOT TRUE `.then(({ rows }) => parseInt(rows[0].count, 10)); +const sqlGetPhotosTagDateRange = async (tag: string) => sql` + SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end + FROM photos + WHERE ${tag}=ANY(tags) AND + hidden IS NOT TRUE +`.then(({ rows }) => rows[0] as PhotoDateRange); + +const sqlGetPhotosCameraDateRange = async (camera: Camera) => sql` + SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end + FROM photos + WHERE + LOWER(make)=${parameterize(camera.make)} AND + LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model)} AND + hidden IS NOT TRUE +`.then(({ rows }) => rows[0] as PhotoDateRange); + const sqlGetUniqueTags = async () => sql` SELECT DISTINCT unnest(tags) as tag FROM photos WHERE hidden IS NOT TRUE @@ -353,10 +370,15 @@ export const getPhoto = async (id: string): Promise => { export const getPhotosCount = () => safelyQueryPhotos(sqlGetPhotosCount); -export const getPhotosCountTag = (tag: string) => - safelyQueryPhotos(() => sqlGetPhotosCountTag(tag)); -export const getPhotosCountCamera = (camera: Camera) => - safelyQueryPhotos(() => sqlGetPhotosCountCamera(camera)); +export const getPhotosTagCount = (tag: string) => + safelyQueryPhotos(() => sqlGetPhotosTagCount(tag)); +export const getPhotosCameraCount = (camera: Camera) => + safelyQueryPhotos(() => sqlGetPhotosCameraCount(camera)); + +export const getPhotosTagDateRange = (tag: string) => + safelyQueryPhotos(() => sqlGetPhotosTagDateRange(tag)); +export const getPhotosCameraDateRange = (camera: Camera) => + safelyQueryPhotos(() => sqlGetPhotosCameraDateRange(camera)); export const getPhotosCountIncludingHidden = () => safelyQueryPhotos(sqlGetPhotosCountIncludingHidden); diff --git a/src/site/pagination.ts b/src/site/pagination.ts index 74e247e5..faa858d4 100644 --- a/src/site/pagination.ts +++ b/src/site/pagination.ts @@ -1,4 +1,4 @@ -type PaginationSearchParams = { next: string }; +export type PaginationSearchParams = { next: string }; export interface PaginationParams { searchParams?: PaginationSearchParams diff --git a/src/tag/TagOverview.tsx b/src/tag/TagOverview.tsx new file mode 100644 index 00000000..e81111cb --- /dev/null +++ b/src/tag/TagOverview.tsx @@ -0,0 +1,41 @@ +import { Photo, PhotoDateRange } from '@/photo'; +import SiteGrid from '@/components/SiteGrid'; +import AnimateItems from '@/components/AnimateItems'; +import PhotoGrid from '@/photo/PhotoGrid'; +import TagHeader from './TagHeader'; + +export default function TagOverview({ + tag, + photos, + count, + dateRange, + showMorePath, + animateOnFirstLoadOnly, +}: { + tag: string, + photos: Photo[], + count: number, + dateRange: PhotoDateRange, + showMorePath?: string, + animateOnFirstLoadOnly?: boolean, +}) { + return ( + + , + ]} + animateOnFirstLoadOnly + /> + + } + /> + ); +} diff --git a/src/tag/data.ts b/src/tag/data.ts new file mode 100644 index 00000000..deb972bb --- /dev/null +++ b/src/tag/data.ts @@ -0,0 +1,52 @@ +import { + getPhotosCached, + getPhotosTagCountCached, + getPhotosTagDateRangeCached, +} from '@/cache'; +import { + PaginationSearchParams, + getPaginationForSearchParams, +} from '@/site/pagination'; +import { pathForTag } from '@/site/paths'; + +export const getPhotosTagDataCached = ({ + tag, + limit, +}: { + tag: string, + limit?: number, +}) => + Promise.all([ + getPhotosCached({ tag, limit }), + getPhotosTagCountCached(tag), + getPhotosTagDateRangeCached(tag), + ]); + +export const getPhotosTagDataCachedWithPagination = async ({ + tag, + limit: limitProp, + searchParams, +}: { + tag: string, + limit?: number, + searchParams?: PaginationSearchParams, +}) => { + const { offset, limit } = getPaginationForSearchParams(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, + }; +}; diff --git a/src/tag/index.ts b/src/tag/index.ts index 5c33844f..a3e96f85 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -1,4 +1,9 @@ -import { Photo, descriptionForPhotoSet, photoQuantityText } from '@/photo'; +import { + Photo, + PhotoDateRange, + descriptionForPhotoSet, + photoQuantityText, +} from '@/photo'; import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths'; import { capitalizeWords } from '@/utility/string'; @@ -15,16 +20,25 @@ export const descriptionForTaggedPhotos = ( photos: Photo[], dateBased?: boolean, explicitCount?: number, + explicitDateRange?: PhotoDateRange, ) => - descriptionForPhotoSet(photos, 'tagged', dateBased, explicitCount); + descriptionForPhotoSet( + photos, + 'tagged', + dateBased, + explicitCount, + explicitDateRange, + ); export const generateMetaForTag = ( tag: string, photos: Photo[], explicitCount?: number, + explicitDateRange?: PhotoDateRange, ) => ({ url: absolutePathForTag(tag), title: titleForTag(tag, photos, explicitCount), - description: descriptionForTaggedPhotos(photos, true), + description: + descriptionForTaggedPhotos(photos, true, explicitCount, explicitDateRange), images: absolutePathForTagImage(tag), });