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),
});