Consolidate camera/tag pagination/date handling
This commit is contained in:
parent
49b871ab13
commit
80823c8d14
@ -10,9 +10,14 @@ import {
|
|||||||
absolutePathForPhotoImage,
|
absolutePathForPhotoImage,
|
||||||
} from '@/site/paths';
|
} from '@/site/paths';
|
||||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||||
import { getPhotoCached, getPhotosCached } from '@/cache';
|
import { getPhotoCached } from '@/cache';
|
||||||
import { getPhotos, getUniqueCameras } from '@/services/postgres';
|
import {
|
||||||
|
getPhotos,
|
||||||
|
getUniqueCameras,
|
||||||
|
} from '@/services/postgres';
|
||||||
import { cameraFromPhoto } from '@/camera';
|
import { cameraFromPhoto } from '@/camera';
|
||||||
|
import { getPhotosCameraDataCached } from '@/camera/data';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
interface PhotoCameraProps {
|
interface PhotoCameraProps {
|
||||||
params: { photoId: string, camera: string }
|
params: { photoId: string, camera: string }
|
||||||
@ -69,19 +74,21 @@ export async function generateMetadata({
|
|||||||
export default async function PhotoCameraPage({
|
export default async function PhotoCameraPage({
|
||||||
params: { photoId, camera: cameraProp },
|
params: { photoId, camera: cameraProp },
|
||||||
children,
|
children,
|
||||||
}: PhotoCameraProps & {
|
}: PhotoCameraProps & { children: ReactNode }) {
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
const photo = await getPhotoCached(photoId);
|
const photo = await getPhotoCached(photoId);
|
||||||
|
|
||||||
if (!photo) { redirect(PATH_ROOT); }
|
if (!photo) { redirect(PATH_ROOT); }
|
||||||
|
|
||||||
const camera = cameraFromPhoto(photo, cameraProp);
|
const camera = cameraFromPhoto(photo, cameraProp);
|
||||||
|
|
||||||
const photos = await getPhotosCached({ camera });
|
const [
|
||||||
|
photos,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
] = await getPhotosCameraDataCached({ camera });
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{children}
|
{children}
|
||||||
<PhotoDetailPage {...{ photo, photos, camera }} />
|
<PhotoDetailPage {...{ photo, photos, camera, count, dateRange }} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
import { getPhotosCached, getPhotosCountCameraCached } from '@/cache';
|
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
|
||||||
import CameraHeader from '@/camera/CameraHeader';
|
|
||||||
import { getMakeModelFromCameraString } from '@/camera';
|
import { getMakeModelFromCameraString } from '@/camera';
|
||||||
import PhotoGrid from '@/photo/PhotoGrid';
|
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { generateMetaForCamera } from '@/camera/meta';
|
import { generateMetaForCamera } from '@/camera/meta';
|
||||||
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
||||||
import { pathForCamera } from '@/site/paths';
|
import { PaginationParams } from '@/site/pagination';
|
||||||
import {
|
import {
|
||||||
PaginationParams,
|
getPhotosCameraDataCached,
|
||||||
getPaginationForSearchParams,
|
getPhotosCameraDataCachedWithPagination,
|
||||||
} from '@/site/pagination';
|
} from '@/camera/data';
|
||||||
|
import CameraOverview from '@/camera/CameraOverview';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
@ -26,17 +23,18 @@ export async function generateMetadata({
|
|||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
] = await Promise.all([
|
dateRange,
|
||||||
getPhotosCached({ camera, limit: GRID_THUMBNAILS_TO_SHOW_MAX }),
|
] = await getPhotosCameraDataCached({
|
||||||
getPhotosCountCameraCached(camera),
|
camera,
|
||||||
]);
|
limit: GRID_THUMBNAILS_TO_SHOW_MAX,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForCamera(camera, photos, count);
|
} = generateMetaForCamera(camera, photos, count, dateRange);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@ -61,27 +59,17 @@ export default async function CameraPage({
|
|||||||
}: CameraProps & PaginationParams) {
|
}: CameraProps & PaginationParams) {
|
||||||
const camera = getMakeModelFromCameraString(params.camera);
|
const camera = getMakeModelFromCameraString(params.camera);
|
||||||
|
|
||||||
const { offset, limit } = getPaginationForSearchParams(searchParams);
|
const {
|
||||||
|
|
||||||
const [
|
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
] = await Promise.all([
|
showMorePath,
|
||||||
getPhotosCached({ camera, limit }),
|
dateRange,
|
||||||
getPhotosCountCameraCached(camera),
|
} = await getPhotosCameraDataCachedWithPagination({
|
||||||
]);
|
camera,
|
||||||
|
searchParams,
|
||||||
const showMorePath = count > photos.length
|
});
|
||||||
? pathForCamera(camera, offset + 1)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteGrid
|
<CameraOverview {...{ camera, photos, count, dateRange, showMorePath }} />
|
||||||
key="Camera Grid"
|
|
||||||
contentMain={<div className="space-y-8 mt-4">
|
|
||||||
<CameraHeader {...{ camera, photos, count }} />
|
|
||||||
<PhotoGrid {...{ photos, camera, showMorePath }} />
|
|
||||||
</div>}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,14 @@
|
|||||||
import { getPhotosCached, getPhotosCountCameraCached } from '@/cache';
|
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
|
||||||
import { cameraFromPhoto, getMakeModelFromCameraString } from '@/camera';
|
import { cameraFromPhoto, getMakeModelFromCameraString } from '@/camera';
|
||||||
import CameraHeader from '@/camera/CameraHeader';
|
|
||||||
import CameraShareModal from '@/camera/CameraShareModal';
|
import CameraShareModal from '@/camera/CameraShareModal';
|
||||||
import { generateMetaForCamera } from '@/camera/meta';
|
import { generateMetaForCamera } from '@/camera/meta';
|
||||||
import PhotoGrid from '@/photo/PhotoGrid';
|
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
||||||
import { pathForCamera } from '@/site/paths';
|
import { PaginationParams } from '@/site/pagination';
|
||||||
import {
|
import {
|
||||||
PaginationParams,
|
getPhotosCameraDataCached,
|
||||||
getPaginationForSearchParams,
|
getPhotosCameraDataCachedWithPagination,
|
||||||
} from '@/site/pagination';
|
} from '@/camera/data';
|
||||||
|
import CameraOverview from '@/camera/CameraOverview';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
@ -27,17 +24,18 @@ export async function generateMetadata({
|
|||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
] = await Promise.all([
|
dateRange,
|
||||||
getPhotosCached({ camera, limit: GRID_THUMBNAILS_TO_SHOW_MAX }),
|
] = await getPhotosCameraDataCached({
|
||||||
getPhotosCountCameraCached(camera),
|
camera,
|
||||||
]);
|
limit: GRID_THUMBNAILS_TO_SHOW_MAX,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForCamera(camera, photos, count);
|
} = generateMetaForCamera(camera, photos, count, dateRange);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@ -62,30 +60,23 @@ export default async function Share({
|
|||||||
}: CameraProps & PaginationParams) {
|
}: CameraProps & PaginationParams) {
|
||||||
const cameraFromParams = getMakeModelFromCameraString(params.camera);
|
const cameraFromParams = getMakeModelFromCameraString(params.camera);
|
||||||
|
|
||||||
const { offset, limit } = getPaginationForSearchParams(searchParams);
|
const {
|
||||||
|
|
||||||
const [
|
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
] = await Promise.all([
|
dateRange,
|
||||||
getPhotosCached({ camera: cameraFromParams, limit }),
|
showMorePath,
|
||||||
getPhotosCountCameraCached(cameraFromParams),
|
} = await getPhotosCameraDataCachedWithPagination({
|
||||||
]);
|
camera: cameraFromParams,
|
||||||
|
searchParams,
|
||||||
|
});
|
||||||
|
|
||||||
const camera = cameraFromPhoto(photos[0], cameraFromParams);
|
const camera = cameraFromPhoto(photos[0], cameraFromParams);
|
||||||
|
|
||||||
const showMorePath = count > photos.length
|
|
||||||
? pathForCamera(camera, offset + 1)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<CameraShareModal {...{ camera, photos, count }} />
|
<CameraShareModal {...{ camera, photos, count, dateRange }} />
|
||||||
<SiteGrid
|
<CameraOverview
|
||||||
key="Camera Grid"
|
{...{ camera, photos, count, dateRange, showMorePath }}
|
||||||
contentMain={<div className="space-y-8 mt-4">
|
animateOnFirstLoadOnly
|
||||||
<CameraHeader {...{ camera, photos, count }} />
|
|
||||||
<PhotoGrid {...{ photos, camera, showMorePath, animate: false }} />
|
|
||||||
</div>}
|
|
||||||
/>
|
/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,11 +10,17 @@ import {
|
|||||||
absolutePathForPhotoImage,
|
absolutePathForPhotoImage,
|
||||||
} from '@/site/paths';
|
} from '@/site/paths';
|
||||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||||
import { getPhotoCached, getPhotosCached } from '@/cache';
|
import { getPhotoCached } from '@/cache';
|
||||||
import { getPhotos, getUniqueTags } from '@/services/postgres';
|
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() {
|
export async function generateStaticParams() {
|
||||||
const params: { params: { photoId: string, tag: string }}[] = [];
|
const params: PhotoTagProps[] = [];
|
||||||
|
|
||||||
const tags = await getUniqueTags();
|
const tags = await getUniqueTags();
|
||||||
tags.forEach(async tag => {
|
tags.forEach(async tag => {
|
||||||
@ -29,9 +35,7 @@ export async function generateStaticParams() {
|
|||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params: { photoId, tag },
|
params: { photoId, tag },
|
||||||
}: {
|
}: PhotoTagProps): Promise<Metadata> {
|
||||||
params: { photoId: string, tag: string }
|
|
||||||
}): Promise<Metadata> {
|
|
||||||
const photo = await getPhotoCached(photoId);
|
const photo = await getPhotoCached(photoId);
|
||||||
|
|
||||||
if (!photo) { return {}; }
|
if (!photo) { return {}; }
|
||||||
@ -62,18 +66,19 @@ export async function generateMetadata({
|
|||||||
export default async function PhotoTagPage({
|
export default async function PhotoTagPage({
|
||||||
params: { photoId, tag },
|
params: { photoId, tag },
|
||||||
children,
|
children,
|
||||||
}: {
|
}: PhotoTagProps & { children: ReactNode }) {
|
||||||
params: { photoId: string, tag: string }
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
const photo = await getPhotoCached(photoId);
|
const photo = await getPhotoCached(photoId);
|
||||||
|
|
||||||
if (!photo) { redirect(PATH_ROOT); }
|
if (!photo) { redirect(PATH_ROOT); }
|
||||||
|
|
||||||
const photos = await getPhotosCached({ tag });
|
const [
|
||||||
|
photos,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
] = await getPhotosTagDataCached({ tag });
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{children}
|
{children}
|
||||||
<PhotoDetailPage {...{ photo, photos, tag }} />
|
<PhotoDetailPage {...{ photo, photos, tag, count, dateRange }} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,12 @@ import { getPhotos, getUniqueTags } from '@/services/postgres';
|
|||||||
import { PATH_ROOT } from '@/site/paths';
|
import { PATH_ROOT } from '@/site/paths';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
interface PhotoTagProps {
|
||||||
|
params: { photoId: string, tag: string }
|
||||||
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const params: { params: { photoId: string, tag: string }}[] = [];
|
const params: PhotoTagProps[] = [];
|
||||||
|
|
||||||
const tags = await getUniqueTags();
|
const tags = await getUniqueTags();
|
||||||
tags.forEach(async tag => {
|
tags.forEach(async tag => {
|
||||||
@ -20,9 +24,7 @@ export async function generateStaticParams() {
|
|||||||
|
|
||||||
export default async function Share({
|
export default async function Share({
|
||||||
params: { photoId, tag },
|
params: { photoId, tag },
|
||||||
}: {
|
}: PhotoTagProps) {
|
||||||
params: { photoId: string, tag: string }
|
|
||||||
}) {
|
|
||||||
const photo = await getPhotoCached(photoId);
|
const photo = await getPhotoCached(photoId);
|
||||||
|
|
||||||
if (!photo) { return redirect(PATH_ROOT); }
|
if (!photo) { return redirect(PATH_ROOT); }
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
import { getPhotosCached, getPhotosCountTagCached } from '@/cache';
|
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
|
||||||
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
||||||
import PhotoGrid from '@/photo/PhotoGrid';
|
import { PaginationParams } from '@/site/pagination';
|
||||||
import {
|
|
||||||
PaginationParams,
|
|
||||||
getPaginationForSearchParams,
|
|
||||||
} from '@/site/pagination';
|
|
||||||
import { pathForTag } from '@/site/paths';
|
|
||||||
import { generateMetaForTag } from '@/tag';
|
import { generateMetaForTag } from '@/tag';
|
||||||
import TagHeader from '@/tag/TagHeader';
|
import TagOverview from '@/tag/TagOverview';
|
||||||
|
import {
|
||||||
|
getPhotosTagDataCached,
|
||||||
|
getPhotosTagDataCachedWithPagination,
|
||||||
|
} from '@/tag/data';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
@ -23,10 +20,10 @@ export async function generateMetadata({
|
|||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
] = await Promise.all([
|
] = await getPhotosTagDataCached({
|
||||||
getPhotosCached({ tag, limit: GRID_THUMBNAILS_TO_SHOW_MAX }),
|
tag,
|
||||||
getPhotosCountTagCached(tag),
|
limit: GRID_THUMBNAILS_TO_SHOW_MAX,
|
||||||
]);
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
@ -56,27 +53,17 @@ export default async function TagPage({
|
|||||||
params: { tag },
|
params: { tag },
|
||||||
searchParams,
|
searchParams,
|
||||||
}:TagProps & PaginationParams) {
|
}:TagProps & PaginationParams) {
|
||||||
const { offset, limit } = getPaginationForSearchParams(searchParams);
|
const {
|
||||||
|
|
||||||
const [
|
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
] = await Promise.all([
|
showMorePath,
|
||||||
getPhotosCached({ tag, limit }),
|
dateRange,
|
||||||
getPhotosCountTagCached(tag),
|
} = await getPhotosTagDataCachedWithPagination({
|
||||||
]);
|
tag,
|
||||||
|
searchParams,
|
||||||
const showMorePath = count > photos.length
|
});
|
||||||
? pathForTag(tag, offset + 1)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteGrid
|
<TagOverview {...{ tag, photos, count, dateRange, showMorePath }} />
|
||||||
key="Tag Grid"
|
|
||||||
contentMain={<div className="space-y-8 mt-4">
|
|
||||||
<TagHeader {...{ tag, photos, count }} />
|
|
||||||
<PhotoGrid {...{ photos, tag, showMorePath }} />
|
|
||||||
</div>}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import { getPhotosCached, getPhotosCountTagCached } from '@/cache';
|
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
|
||||||
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
||||||
import PhotoGrid from '@/photo/PhotoGrid';
|
import { PaginationParams } from '@/site/pagination';
|
||||||
import {
|
|
||||||
PaginationParams,
|
|
||||||
getPaginationForSearchParams,
|
|
||||||
} from '@/site/pagination';
|
|
||||||
import { pathForTag } from '@/site/paths';
|
|
||||||
import { generateMetaForTag } from '@/tag';
|
import { generateMetaForTag } from '@/tag';
|
||||||
import TagHeader from '@/tag/TagHeader';
|
import TagOverview from '@/tag/TagOverview';
|
||||||
import TagShareModal from '@/tag/TagShareModal';
|
import TagShareModal from '@/tag/TagShareModal';
|
||||||
|
import {
|
||||||
|
getPhotosTagDataCached,
|
||||||
|
getPhotosTagDataCachedWithPagination,
|
||||||
|
} from '@/tag/data';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
@ -24,17 +21,18 @@ export async function generateMetadata({
|
|||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
] = await Promise.all([
|
dateRange,
|
||||||
getPhotosCached({ tag, limit: GRID_THUMBNAILS_TO_SHOW_MAX }),
|
] = await getPhotosTagDataCached({
|
||||||
getPhotosCountTagCached(tag),
|
tag,
|
||||||
]);
|
limit: GRID_THUMBNAILS_TO_SHOW_MAX,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForTag(tag, photos, count);
|
} = generateMetaForTag(tag, photos, count, dateRange);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@ -57,28 +55,21 @@ export default async function Share({
|
|||||||
params: { tag },
|
params: { tag },
|
||||||
searchParams,
|
searchParams,
|
||||||
}: TagProps & PaginationParams) {
|
}: TagProps & PaginationParams) {
|
||||||
const { offset, limit } = getPaginationForSearchParams(searchParams);
|
const {
|
||||||
|
|
||||||
const [
|
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
] = await Promise.all([
|
dateRange,
|
||||||
getPhotosCached({ tag, limit }),
|
showMorePath,
|
||||||
getPhotosCountTagCached(tag),
|
} = await getPhotosTagDataCachedWithPagination({
|
||||||
]);
|
tag,
|
||||||
|
searchParams,
|
||||||
const showMorePath = count > photos.length
|
});
|
||||||
? pathForTag(tag, offset + 1)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<TagShareModal {...{ tag, photos, count }} />
|
<TagShareModal {...{ tag, photos, count }} />
|
||||||
<SiteGrid
|
<TagOverview
|
||||||
key="Tag Grid"
|
{...{ tag, photos, count, dateRange, showMorePath }}
|
||||||
contentMain={<div className="space-y-8 mt-4">
|
animateOnFirstLoadOnly
|
||||||
<TagHeader {...{ tag, photos }} />
|
|
||||||
<PhotoGrid {...{ photos, tag, showMorePath, animate: false }} />
|
|
||||||
</div>}
|
|
||||||
/>
|
/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/cache/index.ts
vendored
67
src/cache/index.ts
vendored
@ -4,11 +4,13 @@ import {
|
|||||||
getPhoto,
|
getPhoto,
|
||||||
getPhotos,
|
getPhotos,
|
||||||
getPhotosCount,
|
getPhotosCount,
|
||||||
getPhotosCountCamera,
|
getPhotosCameraCount,
|
||||||
getPhotosCountIncludingHidden,
|
getPhotosCountIncludingHidden,
|
||||||
getPhotosCountTag,
|
getPhotosTagCount,
|
||||||
getUniqueCameras,
|
getUniqueCameras,
|
||||||
getUniqueTags,
|
getUniqueTags,
|
||||||
|
getPhotosTagDateRange,
|
||||||
|
getPhotosCameraDateRange,
|
||||||
} from '@/services/postgres';
|
} from '@/services/postgres';
|
||||||
import { parseCachedPhotosDates, parseCachedPhotoDates } from '@/photo';
|
import { parseCachedPhotosDates, parseCachedPhotoDates } from '@/photo';
|
||||||
import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob';
|
import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob';
|
||||||
@ -16,7 +18,8 @@ import { AuthSession } from 'next-auth';
|
|||||||
import { Camera, createCameraKey } from '@/camera';
|
import { Camera, createCameraKey } from '@/camera';
|
||||||
|
|
||||||
const TAG_PHOTOS = 'photos';
|
const TAG_PHOTOS = 'photos';
|
||||||
const TAG_PHOTOS_COUNT = 'photos-count';
|
const TAG_PHOTOS_COUNT = `${TAG_PHOTOS}-count`;
|
||||||
|
const TAG_PHOTOS_DATE_RANGE = `${TAG_PHOTOS}-date-range`;;
|
||||||
const TAG_TAGS = 'tags';
|
const TAG_TAGS = 'tags';
|
||||||
const TAG_CAMERAS = 'cameras';
|
const TAG_CAMERAS = 'cameras';
|
||||||
const TAG_BLOB = 'blob';
|
const TAG_BLOB = 'blob';
|
||||||
@ -69,6 +72,12 @@ const getPhotoTagCountTag = (tag: string) =>
|
|||||||
const getPhotoCameraCountTag = ({ make, model }: Camera) =>
|
const getPhotoCameraCountTag = ({ make, model }: Camera) =>
|
||||||
`${TAG_PHOTOS_COUNT}-${TAG_CAMERAS}-${createCameraKey(make, model)}`;
|
`${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 = () =>
|
export const revalidatePhotosTag = () =>
|
||||||
revalidateTag(TAG_PHOTOS);
|
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 =
|
export const getPhotosCountIncludingHiddenCached: typeof getPhotosCount =
|
||||||
(...args) =>
|
(...args) =>
|
||||||
unstable_cache(
|
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) =>
|
export const getPhotoCached: typeof getPhoto = (...args) =>
|
||||||
unstable_cache(
|
unstable_cache(
|
||||||
() => getPhoto(...args),
|
() => getPhoto(...args),
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Photo } from '@/photo';
|
import { Photo, PhotoDateRange } from '@/photo';
|
||||||
import { pathForCameraShare } from '@/site/paths';
|
import { pathForCameraShare } from '@/site/paths';
|
||||||
import PhotoHeader from '@/photo/PhotoHeader';
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
import { Camera, cameraFromPhoto } from '.';
|
import { Camera, cameraFromPhoto } from '.';
|
||||||
@ -10,22 +10,26 @@ export default function CameraHeader({
|
|||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
count,
|
count,
|
||||||
|
dateRange,
|
||||||
}: {
|
}: {
|
||||||
camera: Camera
|
camera: Camera
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
selectedPhoto?: Photo
|
selectedPhoto?: Photo
|
||||||
count?: number
|
count?: number
|
||||||
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
const camera = cameraFromPhoto(photos[0], cameraProp);
|
const camera = cameraFromPhoto(photos[0], cameraProp);
|
||||||
return (
|
return (
|
||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
entity={<PhotoCamera {...{ camera }} />}
|
entity={<PhotoCamera {...{ camera }} />}
|
||||||
entityVerb="Photo"
|
entityVerb="Photo"
|
||||||
entityDescription={descriptionForCameraPhotos(photos, undefined, count)}
|
entityDescription={
|
||||||
|
descriptionForCameraPhotos(photos, undefined, count, dateRange)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
sharePath={pathForCameraShare(camera)}
|
sharePath={pathForCameraShare(camera)}
|
||||||
count={count}
|
count={count}
|
||||||
|
dateRange={dateRange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Photo } from '@/photo';
|
import { Photo, PhotoDateRange } from '@/photo';
|
||||||
import { absolutePathForCameraImage, pathForCamera } from '@/site/paths';
|
import { absolutePathForCameraImage, pathForCamera } from '@/site/paths';
|
||||||
import OGTile from '@/components/OGTile';
|
import OGTile from '@/components/OGTile';
|
||||||
import { Camera } from '.';
|
import { Camera } from '.';
|
||||||
@ -15,6 +15,7 @@ export default function CameraOGTile({
|
|||||||
onFail,
|
onFail,
|
||||||
retryTime,
|
retryTime,
|
||||||
count,
|
count,
|
||||||
|
dateRange,
|
||||||
}: {
|
}: {
|
||||||
camera: Camera
|
camera: Camera
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
@ -24,11 +25,12 @@ export default function CameraOGTile({
|
|||||||
riseOnHover?: boolean
|
riseOnHover?: boolean
|
||||||
retryTime?: number
|
retryTime?: number
|
||||||
count?: number
|
count?: number
|
||||||
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<OGTile {...{
|
<OGTile {...{
|
||||||
title: titleForCamera(camera, photos, count),
|
title: titleForCamera(camera, photos, count),
|
||||||
description: descriptionForCameraPhotos(photos, true, count),
|
description: descriptionForCameraPhotos(photos, true, count, dateRange),
|
||||||
path: pathForCamera(camera),
|
path: pathForCamera(camera),
|
||||||
pathImageAbsolute: absolutePathForCameraImage(camera),
|
pathImageAbsolute: absolutePathForCameraImage(camera),
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
|
|||||||
42
src/camera/CameraOverview.tsx
Normal file
42
src/camera/CameraOverview.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Photo, PhotoDateRange } from '@/photo';
|
||||||
|
import { Camera } from '.';
|
||||||
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
|
import AnimateItems from '@/components/AnimateItems';
|
||||||
|
import CameraHeader from './CameraHeader';
|
||||||
|
import PhotoGrid from '@/photo/PhotoGrid';
|
||||||
|
|
||||||
|
export default function CameraOverview({
|
||||||
|
camera,
|
||||||
|
photos,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
showMorePath,
|
||||||
|
animateOnFirstLoadOnly,
|
||||||
|
}: {
|
||||||
|
camera: Camera,
|
||||||
|
photos: Photo[],
|
||||||
|
count: number,
|
||||||
|
dateRange: PhotoDateRange,
|
||||||
|
showMorePath?: string,
|
||||||
|
animateOnFirstLoadOnly?: boolean,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SiteGrid
|
||||||
|
contentMain={<div className="space-y-8 mt-4">
|
||||||
|
<AnimateItems
|
||||||
|
type="bottom"
|
||||||
|
items={[
|
||||||
|
<CameraHeader
|
||||||
|
key="CameraHeader"
|
||||||
|
{...{ camera, photos, count, dateRange }}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
animateOnFirstLoadOnly
|
||||||
|
/>
|
||||||
|
<PhotoGrid
|
||||||
|
{...{ photos, camera, showMorePath, animateOnFirstLoadOnly }}
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { absolutePathForCamera, pathForCamera } from '@/site/paths';
|
import { absolutePathForCamera, pathForCamera } from '@/site/paths';
|
||||||
import { Photo } from '../photo';
|
import { Photo, PhotoDateRange } from '../photo';
|
||||||
import ShareModal from '@/components/ShareModal';
|
import ShareModal from '@/components/ShareModal';
|
||||||
import CameraOGTile from './CameraOGTile';
|
import CameraOGTile from './CameraOGTile';
|
||||||
import { Camera } from '.';
|
import { Camera } from '.';
|
||||||
@ -8,10 +8,12 @@ export default function CameraShareModal({
|
|||||||
camera,
|
camera,
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
|
dateRange,
|
||||||
}: {
|
}: {
|
||||||
camera: Camera
|
camera: Camera
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
count?: number
|
count: number
|
||||||
|
dateRange: PhotoDateRange,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
@ -19,7 +21,7 @@ export default function CameraShareModal({
|
|||||||
pathShare={absolutePathForCamera(camera)}
|
pathShare={absolutePathForCamera(camera)}
|
||||||
pathClose={pathForCamera(camera)}
|
pathClose={pathForCamera(camera)}
|
||||||
>
|
>
|
||||||
<CameraOGTile {...{ camera, photos, count }} />
|
<CameraOGTile {...{ camera, photos, count, dateRange }} />
|
||||||
</ShareModal>
|
</ShareModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
53
src/camera/data.ts
Normal file
53
src/camera/data.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,4 +1,9 @@
|
|||||||
import { Photo, descriptionForPhotoSet, photoQuantityText } from '@/photo';
|
import {
|
||||||
|
Photo,
|
||||||
|
PhotoDateRange,
|
||||||
|
descriptionForPhotoSet,
|
||||||
|
photoQuantityText,
|
||||||
|
} from '@/photo';
|
||||||
import { Camera, cameraFromPhoto, formatCameraText } from '.';
|
import { Camera, cameraFromPhoto, formatCameraText } from '.';
|
||||||
import {
|
import {
|
||||||
absolutePathForCamera,
|
absolutePathForCamera,
|
||||||
@ -23,16 +28,25 @@ export const descriptionForCameraPhotos = (
|
|||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
|
explicitDateRange?: PhotoDateRange,
|
||||||
) =>
|
) =>
|
||||||
descriptionForPhotoSet(photos, undefined, dateBased, explicitCount);
|
descriptionForPhotoSet(
|
||||||
|
photos,
|
||||||
|
undefined,
|
||||||
|
dateBased,
|
||||||
|
explicitCount,
|
||||||
|
explicitDateRange,
|
||||||
|
);
|
||||||
|
|
||||||
export const generateMetaForCamera = (
|
export const generateMetaForCamera = (
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
|
explicitDateRange?: PhotoDateRange,
|
||||||
) => ({
|
) => ({
|
||||||
url: absolutePathForCamera(camera),
|
url: absolutePathForCamera(camera),
|
||||||
title: titleForCamera(camera, photos, explicitCount),
|
title: titleForCamera(camera, photos, explicitCount),
|
||||||
description: descriptionForCameraPhotos(photos, true, explicitCount),
|
description:
|
||||||
|
descriptionForCameraPhotos(photos, true, explicitCount, explicitDateRange),
|
||||||
images: absolutePathForCameraImage(camera),
|
images: absolutePathForCameraImage(camera),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { Variant, motion } from 'framer-motion';
|
||||||
import { useAppState } from '@/state';
|
import { useAppState } from '@/state';
|
||||||
|
|
||||||
export type AnimationType = 'none' | 'scale' | 'left' | 'right';
|
export type AnimationType = 'none' | 'scale' | 'left' | 'right' | 'bottom';
|
||||||
|
|
||||||
export interface AnimationConfig {
|
export interface AnimationConfig {
|
||||||
type?: AnimationType
|
type?: AnimationType
|
||||||
@ -58,7 +58,7 @@ function AnimateItems({
|
|||||||
? (nextPhotoAnimationInitial.current?.duration ?? duration)
|
? (nextPhotoAnimationInitial.current?.duration ?? duration)
|
||||||
: duration;
|
: duration;
|
||||||
|
|
||||||
const getInitialVariant = () => {
|
const getInitialVariant = (): Variant => {
|
||||||
switch (typeResolved) {
|
switch (typeResolved) {
|
||||||
case 'left': return {
|
case 'left': return {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@ -68,6 +68,10 @@ function AnimateItems({
|
|||||||
opacity: 0,
|
opacity: 0,
|
||||||
transform: `translateX(${-distanceOffset}px)`,
|
transform: `translateX(${-distanceOffset}px)`,
|
||||||
};
|
};
|
||||||
|
case 'bottom': return {
|
||||||
|
opacity: 0,
|
||||||
|
transform: `translateY(${distanceOffset}px)`,
|
||||||
|
};
|
||||||
default: return {
|
default: return {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transform: `translateY(${distanceOffset}px) scale(${scaleOffset})`,
|
transform: `translateY(${distanceOffset}px) scale(${scaleOffset})`,
|
||||||
@ -98,7 +102,6 @@ function AnimateItems({
|
|||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
className={classNameItem}
|
className={classNameItem}
|
||||||
// style={getInitialVariant()}
|
|
||||||
variants={{
|
variants={{
|
||||||
hidden: getInitialVariant(),
|
hidden: getInitialVariant(),
|
||||||
show: {
|
show: {
|
||||||
|
|||||||
@ -47,8 +47,7 @@ export default function Nav({ showTextLinks }: { showTextLinks?: boolean }) {
|
|||||||
<SiteGrid
|
<SiteGrid
|
||||||
contentMain={
|
contentMain={
|
||||||
<AnimateItems
|
<AnimateItems
|
||||||
type={!shouldAnimate ? 'none' : undefined}
|
type={!shouldAnimate ? 'none' : 'bottom'}
|
||||||
scaleOffset={1}
|
|
||||||
distanceOffset={10}
|
distanceOffset={10}
|
||||||
items={showNav
|
items={showNav
|
||||||
? [<div
|
? [<div
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import AnimateItems from '@/components/AnimateItems';
|
import AnimateItems from '@/components/AnimateItems';
|
||||||
import { Photo } from '.';
|
import { Photo, PhotoDateRange } from '.';
|
||||||
import PhotoLarge from './PhotoLarge';
|
import PhotoLarge from './PhotoLarge';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import PhotoGrid from './PhotoGrid';
|
import PhotoGrid from './PhotoGrid';
|
||||||
@ -15,12 +15,16 @@ export default function PhotoDetailPage({
|
|||||||
photosGrid,
|
photosGrid,
|
||||||
tag,
|
tag,
|
||||||
camera,
|
camera,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
}: {
|
}: {
|
||||||
photo: Photo
|
photo: Photo
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
photosGrid?: Photo[]
|
photosGrid?: Photo[]
|
||||||
tag?: string
|
tag?: string
|
||||||
camera?: Camera
|
camera?: Camera
|
||||||
|
count?: number
|
||||||
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -40,10 +44,11 @@ export default function PhotoDetailPage({
|
|||||||
className="mt-4 mb-8"
|
className="mt-4 mb-8"
|
||||||
contentMain={
|
contentMain={
|
||||||
<CameraHeader
|
<CameraHeader
|
||||||
key={tag}
|
|
||||||
camera={camera}
|
camera={camera}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={photo}
|
selectedPhoto={photo}
|
||||||
|
count={count}
|
||||||
|
dateRange={dateRange}
|
||||||
/>}
|
/>}
|
||||||
/>}
|
/>}
|
||||||
<AnimateItems
|
<AnimateItems
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { cc } from '@/utility/css';
|
import { cc } from '@/utility/css';
|
||||||
import { Photo, dateRangeForPhotos } from '.';
|
import { Photo, PhotoDateRange, dateRangeForPhotos } from '.';
|
||||||
import ShareButton from '@/components/ShareButton';
|
import ShareButton from '@/components/ShareButton';
|
||||||
|
|
||||||
export default function PhotoHeader({
|
export default function PhotoHeader({
|
||||||
@ -10,6 +10,7 @@ export default function PhotoHeader({
|
|||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
sharePath,
|
sharePath,
|
||||||
count,
|
count,
|
||||||
|
dateRange,
|
||||||
}: {
|
}: {
|
||||||
entity: JSX.Element
|
entity: JSX.Element
|
||||||
entityVerb: string
|
entityVerb: string
|
||||||
@ -18,8 +19,9 @@ export default function PhotoHeader({
|
|||||||
selectedPhoto?: Photo
|
selectedPhoto?: Photo
|
||||||
sharePath: string
|
sharePath: string
|
||||||
count?: number
|
count?: number
|
||||||
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
const { start, end } = dateRangeForPhotos(photos);
|
const { start, end } = dateRangeForPhotos(photos, dateRange);
|
||||||
|
|
||||||
const selectedPhotoIndex = selectedPhoto
|
const selectedPhotoIndex = selectedPhoto
|
||||||
? photos.findIndex(photo => photo.id === selectedPhoto.id)
|
? photos.findIndex(photo => photo.id === selectedPhoto.id)
|
||||||
|
|||||||
@ -60,7 +60,6 @@ export interface Photo extends PhotoDb {
|
|||||||
exposureTimeFormatted?: string
|
exposureTimeFormatted?: string
|
||||||
exposureCompensationFormatted?: string
|
exposureCompensationFormatted?: string
|
||||||
takenAtNaiveFormatted: string
|
takenAtNaiveFormatted: string
|
||||||
takenAtNaiveFormattedShort: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
||||||
@ -84,8 +83,6 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
|||||||
formatExposureCompensation(photoDb.exposureCompensation),
|
formatExposureCompensation(photoDb.exposureCompensation),
|
||||||
takenAtNaiveFormatted:
|
takenAtNaiveFormatted:
|
||||||
formatDateFromPostgresString(photoDb.takenAtNaive),
|
formatDateFromPostgresString(photoDb.takenAtNaive),
|
||||||
takenAtNaiveFormattedShort:
|
|
||||||
formatDateFromPostgresString(photoDb.takenAtNaive, true),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -163,23 +160,35 @@ const photoLabelForCount = (count: number) =>
|
|||||||
export const photoQuantityText = (count: number) =>
|
export const photoQuantityText = (count: number) =>
|
||||||
`(${count} ${photoLabelForCount(count)})`;
|
`(${count} ${photoLabelForCount(count)})`;
|
||||||
|
|
||||||
|
export type PhotoDateRange = { start: string, end: string };
|
||||||
|
|
||||||
export const descriptionForPhotoSet = (
|
export const descriptionForPhotoSet = (
|
||||||
photos:Photo[],
|
photos:Photo[],
|
||||||
descriptor?: string,
|
descriptor?: string,
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
|
explicitDateRange?: PhotoDateRange,
|
||||||
) =>
|
) =>
|
||||||
dateBased
|
dateBased
|
||||||
? dateRangeForPhotos(photos).description.toUpperCase()
|
? dateRangeForPhotos(photos, explicitDateRange).description.toUpperCase()
|
||||||
: [
|
: [
|
||||||
explicitCount ?? photos.length,
|
explicitCount ?? photos.length,
|
||||||
descriptor,
|
descriptor,
|
||||||
photoLabelForCount(explicitCount ?? photos.length),
|
photoLabelForCount(explicitCount ?? photos.length),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const dateRangeForPhotos = (photos: Photo[]) => {
|
export const dateRangeForPhotos = (
|
||||||
const start = photos[0].takenAtNaiveFormattedShort;
|
photos: Photo[],
|
||||||
const end = photos[photos.length - 1].takenAtNaiveFormattedShort;
|
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
|
const description = start === end
|
||||||
? start
|
? start
|
||||||
: `${start}–${end}`;
|
: `${start}–${end}`;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
translatePhotoId,
|
translatePhotoId,
|
||||||
parsePhotoFromDb,
|
parsePhotoFromDb,
|
||||||
Photo,
|
Photo,
|
||||||
|
PhotoDateRange,
|
||||||
} from '@/photo';
|
} from '@/photo';
|
||||||
import { Camera, createCameraKey } from '@/camera';
|
import { Camera, createCameraKey } from '@/camera';
|
||||||
import { parameterize } from '@/utility/string';
|
import { parameterize } from '@/utility/string';
|
||||||
@ -239,13 +240,13 @@ const sqlGetPhotosCountIncludingHidden = async () => sql`
|
|||||||
SELECT COUNT(*) FROM photos
|
SELECT COUNT(*) FROM photos
|
||||||
`.then(({ rows }) => parseInt(rows[0].count, 10));
|
`.then(({ rows }) => parseInt(rows[0].count, 10));
|
||||||
|
|
||||||
const sqlGetPhotosCountTag = async (tag: string) => sql`
|
const sqlGetPhotosTagCount = async (tag: string) => sql`
|
||||||
SELECT COUNT(*) FROM photos
|
SELECT COUNT(*) FROM photos
|
||||||
WHERE ${tag}=ANY(tags) AND
|
WHERE ${tag}=ANY(tags) AND
|
||||||
hidden IS NOT TRUE
|
hidden IS NOT TRUE
|
||||||
`.then(({ rows }) => parseInt(rows[0].count, 10));
|
`.then(({ rows }) => parseInt(rows[0].count, 10));
|
||||||
|
|
||||||
const sqlGetPhotosCountCamera = async (camera: Camera) => sql`
|
const sqlGetPhotosCameraCount = async (camera: Camera) => sql`
|
||||||
SELECT COUNT(*) FROM photos
|
SELECT COUNT(*) FROM photos
|
||||||
WHERE
|
WHERE
|
||||||
LOWER(make)=${parameterize(camera.make)} AND
|
LOWER(make)=${parameterize(camera.make)} AND
|
||||||
@ -253,6 +254,22 @@ const sqlGetPhotosCountCamera = async (camera: Camera) => sql`
|
|||||||
hidden IS NOT TRUE
|
hidden IS NOT TRUE
|
||||||
`.then(({ rows }) => parseInt(rows[0].count, 10));
|
`.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`
|
const sqlGetUniqueTags = async () => sql`
|
||||||
SELECT DISTINCT unnest(tags) as tag FROM photos
|
SELECT DISTINCT unnest(tags) as tag FROM photos
|
||||||
WHERE hidden IS NOT TRUE
|
WHERE hidden IS NOT TRUE
|
||||||
@ -353,10 +370,15 @@ export const getPhoto = async (id: string): Promise<Photo | undefined> => {
|
|||||||
|
|
||||||
export const getPhotosCount = () =>
|
export const getPhotosCount = () =>
|
||||||
safelyQueryPhotos(sqlGetPhotosCount);
|
safelyQueryPhotos(sqlGetPhotosCount);
|
||||||
export const getPhotosCountTag = (tag: string) =>
|
export const getPhotosTagCount = (tag: string) =>
|
||||||
safelyQueryPhotos(() => sqlGetPhotosCountTag(tag));
|
safelyQueryPhotos(() => sqlGetPhotosTagCount(tag));
|
||||||
export const getPhotosCountCamera = (camera: Camera) =>
|
export const getPhotosCameraCount = (camera: Camera) =>
|
||||||
safelyQueryPhotos(() => sqlGetPhotosCountCamera(camera));
|
safelyQueryPhotos(() => sqlGetPhotosCameraCount(camera));
|
||||||
|
|
||||||
|
export const getPhotosTagDateRange = (tag: string) =>
|
||||||
|
safelyQueryPhotos(() => sqlGetPhotosTagDateRange(tag));
|
||||||
|
export const getPhotosCameraDateRange = (camera: Camera) =>
|
||||||
|
safelyQueryPhotos(() => sqlGetPhotosCameraDateRange(camera));
|
||||||
|
|
||||||
export const getPhotosCountIncludingHidden = () =>
|
export const getPhotosCountIncludingHidden = () =>
|
||||||
safelyQueryPhotos(sqlGetPhotosCountIncludingHidden);
|
safelyQueryPhotos(sqlGetPhotosCountIncludingHidden);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
type PaginationSearchParams = { next: string };
|
export type PaginationSearchParams = { next: string };
|
||||||
|
|
||||||
export interface PaginationParams {
|
export interface PaginationParams {
|
||||||
searchParams?: PaginationSearchParams
|
searchParams?: PaginationSearchParams
|
||||||
|
|||||||
41
src/tag/TagOverview.tsx
Normal file
41
src/tag/TagOverview.tsx
Normal file
@ -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 (
|
||||||
|
<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>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/tag/data.ts
Normal file
52
src/tag/data.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,4 +1,9 @@
|
|||||||
import { Photo, descriptionForPhotoSet, photoQuantityText } from '@/photo';
|
import {
|
||||||
|
Photo,
|
||||||
|
PhotoDateRange,
|
||||||
|
descriptionForPhotoSet,
|
||||||
|
photoQuantityText,
|
||||||
|
} from '@/photo';
|
||||||
import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths';
|
import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths';
|
||||||
import { capitalizeWords } from '@/utility/string';
|
import { capitalizeWords } from '@/utility/string';
|
||||||
|
|
||||||
@ -15,16 +20,25 @@ export const descriptionForTaggedPhotos = (
|
|||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
|
explicitDateRange?: PhotoDateRange,
|
||||||
) =>
|
) =>
|
||||||
descriptionForPhotoSet(photos, 'tagged', dateBased, explicitCount);
|
descriptionForPhotoSet(
|
||||||
|
photos,
|
||||||
|
'tagged',
|
||||||
|
dateBased,
|
||||||
|
explicitCount,
|
||||||
|
explicitDateRange,
|
||||||
|
);
|
||||||
|
|
||||||
export const generateMetaForTag = (
|
export const generateMetaForTag = (
|
||||||
tag: string,
|
tag: string,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
|
explicitDateRange?: PhotoDateRange,
|
||||||
) => ({
|
) => ({
|
||||||
url: absolutePathForTag(tag),
|
url: absolutePathForTag(tag),
|
||||||
title: titleForTag(tag, photos, explicitCount),
|
title: titleForTag(tag, photos, explicitCount),
|
||||||
description: descriptionForTaggedPhotos(photos, true),
|
description:
|
||||||
|
descriptionForTaggedPhotos(photos, true, explicitCount, explicitDateRange),
|
||||||
images: absolutePathForTagImage(tag),
|
images: absolutePathForTagImage(tag),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user