Consolidate camera/tag pagination/date handling

This commit is contained in:
Sam Becker 2023-10-04 19:01:17 -05:00
parent 49b871ab13
commit 80823c8d14
24 changed files with 469 additions and 207 deletions

View File

@ -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}
<PhotoDetailPage {...{ photo, photos, camera }} />
<PhotoDetailPage {...{ photo, photos, camera, count, dateRange }} />
</>;
}

View File

@ -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 (
<SiteGrid
key="Camera Grid"
contentMain={<div className="space-y-8 mt-4">
<CameraHeader {...{ camera, photos, count }} />
<PhotoGrid {...{ photos, camera, showMorePath }} />
</div>}
/>
<CameraOverview {...{ camera, photos, count, dateRange, showMorePath }} />
);
}

View File

@ -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 <>
<CameraShareModal {...{ camera, photos, count }} />
<SiteGrid
key="Camera Grid"
contentMain={<div className="space-y-8 mt-4">
<CameraHeader {...{ camera, photos, count }} />
<PhotoGrid {...{ photos, camera, showMorePath, animate: false }} />
</div>}
<CameraShareModal {...{ camera, photos, count, dateRange }} />
<CameraOverview
{...{ camera, photos, count, dateRange, showMorePath }}
animateOnFirstLoadOnly
/>
</>;
}

View File

@ -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<Metadata> {
}: PhotoTagProps): Promise<Metadata> {
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}
<PhotoDetailPage {...{ photo, photos, tag }} />
<PhotoDetailPage {...{ photo, photos, tag, count, dateRange }} />
</>;
}

View File

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

View File

@ -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 (
<SiteGrid
key="Tag Grid"
contentMain={<div className="space-y-8 mt-4">
<TagHeader {...{ tag, photos, count }} />
<PhotoGrid {...{ photos, tag, showMorePath }} />
</div>}
/>
<TagOverview {...{ tag, photos, count, dateRange, showMorePath }} />
);
}

View File

@ -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 <>
<TagShareModal {...{ tag, photos, count }} />
<SiteGrid
key="Tag Grid"
contentMain={<div className="space-y-8 mt-4">
<TagHeader {...{ tag, photos }} />
<PhotoGrid {...{ photos, tag, showMorePath, animate: false }} />
</div>}
<TagOverview
{...{ tag, photos, count, dateRange, showMorePath }}
animateOnFirstLoadOnly
/>
</>;
}

75
src/cache/index.ts vendored
View File

@ -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),

View File

@ -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 (
<PhotoHeader
entity={<PhotoCamera {...{ camera }} />}
entityVerb="Photo"
entityDescription={descriptionForCameraPhotos(photos, undefined, count)}
entityDescription={
descriptionForCameraPhotos(photos, undefined, count, dateRange)}
photos={photos}
selectedPhoto={selectedPhoto}
sharePath={pathForCameraShare(camera)}
count={count}
dateRange={dateRange}
/>
);
}

View File

@ -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 (
<OGTile {...{
title: titleForCamera(camera, photos, count),
description: descriptionForCameraPhotos(photos, true, count),
description: descriptionForCameraPhotos(photos, true, count, dateRange),
path: pathForCamera(camera),
pathImageAbsolute: absolutePathForCameraImage(camera),
loadingState: loadingStateExternal,

View 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>}
/>
);
}

View File

@ -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 (
<ShareModal
@ -19,7 +21,7 @@ export default function CameraShareModal({
pathShare={absolutePathForCamera(camera)}
pathClose={pathForCamera(camera)}
>
<CameraOGTile {...{ camera, photos, count }} />
<CameraOGTile {...{ camera, photos, count, dateRange }} />
</ShareModal>
);
};

53
src/camera/data.ts Normal file
View 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,
};
};

View File

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

View File

@ -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({
<motion.div
key={index}
className={classNameItem}
// style={getInitialVariant()}
variants={{
hidden: getInitialVariant(),
show: {

View File

@ -47,8 +47,7 @@ export default function Nav({ showTextLinks }: { showTextLinks?: boolean }) {
<SiteGrid
contentMain={
<AnimateItems
type={!shouldAnimate ? 'none' : undefined}
scaleOffset={1}
type={!shouldAnimate ? 'none' : 'bottom'}
distanceOffset={10}
items={showNav
? [<div

View File

@ -1,5 +1,5 @@
import AnimateItems from '@/components/AnimateItems';
import { Photo } from '.';
import { Photo, PhotoDateRange } from '.';
import PhotoLarge from './PhotoLarge';
import SiteGrid from '@/components/SiteGrid';
import PhotoGrid from './PhotoGrid';
@ -15,12 +15,16 @@ export default function PhotoDetailPage({
photosGrid,
tag,
camera,
count,
dateRange,
}: {
photo: Photo
photos: Photo[]
photosGrid?: Photo[]
tag?: string
camera?: Camera
count?: number
dateRange?: PhotoDateRange
}) {
return (
<div>
@ -40,10 +44,11 @@ export default function PhotoDetailPage({
className="mt-4 mb-8"
contentMain={
<CameraHeader
key={tag}
camera={camera}
photos={photos}
selectedPhoto={photo}
count={count}
dateRange={dateRange}
/>}
/>}
<AnimateItems

View File

@ -1,5 +1,5 @@
import { cc } from '@/utility/css';
import { Photo, dateRangeForPhotos } from '.';
import { Photo, PhotoDateRange, dateRangeForPhotos } from '.';
import ShareButton from '@/components/ShareButton';
export default function PhotoHeader({
@ -10,6 +10,7 @@ export default function PhotoHeader({
selectedPhoto,
sharePath,
count,
dateRange,
}: {
entity: JSX.Element
entityVerb: string
@ -18,8 +19,9 @@ export default function PhotoHeader({
selectedPhoto?: Photo
sharePath: string
count?: number
dateRange?: PhotoDateRange
}) {
const { start, end } = dateRangeForPhotos(photos);
const { start, end } = dateRangeForPhotos(photos, dateRange);
const selectedPhotoIndex = selectedPhoto
? photos.findIndex(photo => photo.id === selectedPhoto.id)

View File

@ -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}`;

View File

@ -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<Photo | undefined> => {
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);

View File

@ -1,4 +1,4 @@
type PaginationSearchParams = { next: string };
export type PaginationSearchParams = { next: string };
export interface PaginationParams {
searchParams?: PaginationSearchParams

41
src/tag/TagOverview.tsx Normal file
View 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
View 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,
};
};

View File

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