Paginate camera and tag views

This commit is contained in:
Sam Becker 2023-10-04 13:14:19 -05:00
parent ee841518ec
commit e93e23f428
23 changed files with 320 additions and 130 deletions

View File

@ -19,7 +19,7 @@ import {
pathForPhoto,
pathForPhotoEdit,
} from '@/site/paths';
import { getPhotosLimitForQuery, titleForPhoto } from '@/photo';
import { titleForPhoto } from '@/photo';
import MorePhotos from '@/components/MorePhotos';
import {
getBlobPhotoUrlsCached,
@ -29,17 +29,17 @@ import {
} from '@/cache';
import { AiOutlineEyeInvisible } from 'react-icons/ai';
import { BiTrash } from 'react-icons/bi';
import {
PaginationParams,
getPaginationForSearchParams,
} from '@/site/pagination';
export const runtime = 'edge';
const DEBUG_PHOTO_BLOBS = false;
export default async function AdminPage({
searchParams,
}: {
searchParams: { next: string };
}) {
const { offset, limit } = getPhotosLimitForQuery(searchParams.next);
export default async function AdminPage({ searchParams }: PaginationParams) {
const { offset, limit } = getPaginationForSearchParams(searchParams);
const [
photos,

View File

@ -5,9 +5,8 @@ import {
getUniqueTagsCached,
} from '@/cache';
import HeaderList from '@/components/HeaderList';
import MorePhotos from '@/components/MorePhotos';
import SiteGrid from '@/components/SiteGrid';
import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo';
import { generateOgImageMetaForPhotos } from '@/photo';
import PhotoCamera from '@/camera/PhotoCamera';
import PhotoGrid from '@/photo/PhotoGrid';
import PhotosEmptyState from '@/photo/PhotosEmptyState';
@ -17,6 +16,10 @@ import PhotoTag from '@/tag/PhotoTag';
import { Metadata } from 'next';
import { FaTag } from 'react-icons/fa';
import { IoMdCamera } from 'react-icons/io';
import {
PaginationParams,
getPaginationForSearchParams,
} from '@/site/pagination';
export const runtime = 'edge';
@ -25,12 +28,8 @@ export async function generateMetadata(): Promise<Metadata> {
return generateOgImageMetaForPhotos(photos);
}
export default async function GridPage({
searchParams,
}: {
searchParams: { next: string };
}) {
const { offset, limit } = getPhotosLimitForQuery(searchParams.next);
export default async function GridPage({ searchParams }: PaginationParams) {
const { offset, limit } = getPaginationForSearchParams(searchParams);
const [
photos,
@ -44,16 +43,14 @@ export default async function GridPage({
getUniqueCamerasCached(),
]);
const showMorePhotos = count > photos.length;
const showMorePath = count > photos.length
? pathForGrid(offset + 1)
: undefined;
return (
photos.length > 0
? <SiteGrid
contentMain={<div className="space-y-4">
<PhotoGrid photos={photos} />
{showMorePhotos &&
<MorePhotos path={pathForGrid(offset + 1)} />}
</div>}
contentMain={<PhotoGrid {...{ photos, showMorePath }} />}
contentSide={<div className="sticky top-4 space-y-4">
{tags.length > 0 && <HeaderList
title='Tags'

View File

@ -1,17 +1,16 @@
import { getPhotosCached, getPhotosCountCached } from '@/cache';
import MorePhotos from '@/components/MorePhotos';
import { getPhotosLimitForQuery } from '@/photo';
import StaggeredOgPhotos from '@/photo/StaggeredOgPhotos';
import {
PaginationParams,
getPaginationForSearchParams,
} from '@/site/pagination';
import { pathForOg } from '@/site/paths';
export const runtime = 'edge';
export default async function GridPage({
searchParams,
}: {
searchParams: { next: string };
}) {
const { offset, limit } = getPhotosLimitForQuery(searchParams.next);
export default async function GridPage({ searchParams }: PaginationParams) {
const { offset, limit } = getPaginationForSearchParams(searchParams);
const [
photos,

View File

@ -2,9 +2,13 @@ import { getPhotosCached, getPhotosCountCached } from '@/cache';
import AnimateItems from '@/components/AnimateItems';
import MorePhotos from '@/components/MorePhotos';
import SiteGrid from '@/components/SiteGrid';
import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo';
import { generateOgImageMetaForPhotos } from '@/photo';
import PhotoLarge from '@/photo/PhotoLarge';
import PhotosEmptyState from '@/photo/PhotosEmptyState';
import {
PaginationParams,
getPaginationForSearchParams,
} from '@/site/pagination';
import { pathForRoot } from '@/site/paths';
import { Metadata } from 'next';
@ -15,12 +19,8 @@ export async function generateMetadata(): Promise<Metadata> {
return generateOgImageMetaForPhotos(photos);
}
export default async function HomePage({
searchParams,
}: {
searchParams: { next: string };
}) {
const { offset, limit } = getPhotosLimitForQuery(searchParams.next, 12);
export default async function HomePage({ searchParams }: PaginationParams) {
const { offset, limit } = getPaginationForSearchParams(searchParams, 12);
const [
photos,

View File

@ -1,4 +1,4 @@
import { getPhotosCached } from '@/cache';
import { getPhotosCached, getPhotosCountCameraCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
import CameraHeader from '@/camera/CameraHeader';
import { getMakeModelFromCameraString } from '@/camera';
@ -6,9 +6,15 @@ import PhotoGrid from '@/photo/PhotoGrid';
import { getUniqueCameras } from '@/services/postgres';
import { Metadata } from 'next';
import { generateMetaForCamera } from '@/camera/meta';
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
import { pathForCamera } from '@/site/paths';
import {
PaginationParams,
getPaginationForSearchParams,
} from '@/site/pagination';
interface CameraProps {
params: { camera: string }
params: { camera: string },
}
export async function generateStaticParams() {
@ -22,14 +28,21 @@ export async function generateMetadata({
params,
}: CameraProps): Promise<Metadata> {
const camera = getMakeModelFromCameraString(params.camera);
const photos = await getPhotosCached({ camera });
const [
photos,
count,
] = await Promise.all([
getPhotosCached({ camera, limit: GRID_THUMBNAILS_TO_SHOW_MAX }),
getPhotosCountCameraCached(camera),
]);
const {
url,
title,
description,
images,
} = generateMetaForCamera(camera, photos);
} = generateMetaForCamera(camera, photos, count);
return {
title,
@ -48,17 +61,32 @@ export async function generateMetadata({
};
}
export default async function CameraPage({ params }:CameraProps) {
export default async function CameraPage({
params,
searchParams,
}: CameraProps & PaginationParams) {
const camera = getMakeModelFromCameraString(params.camera);
const photos = await getPhotosCached({ camera });
const { offset, limit } = getPaginationForSearchParams(searchParams);
const [
photos,
count,
] = await Promise.all([
getPhotosCached({ camera, limit }),
getPhotosCountCameraCached(camera),
]);
const showMorePath = count > photos.length
? pathForCamera(camera, offset + 1)
: undefined;
return (
<SiteGrid
key="Camera Grid"
contentMain={<div className="space-y-8 mt-4">
<CameraHeader camera={camera} photos={photos} />
<PhotoGrid photos={photos} camera={camera} />
<CameraHeader {...{ camera, photos, count }} />
<PhotoGrid {...{ photos, camera, showMorePath }} />
</div>}
/>
);

View File

@ -1,4 +1,4 @@
import { getPhotosCached } from '@/cache';
import { getPhotosCached, getPhotosCountCameraCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
import { cameraFromPhoto, getMakeModelFromCameraString } from '@/camera';
import CameraHeader from '@/camera/CameraHeader';
@ -7,6 +7,12 @@ import { generateMetaForCamera } from '@/camera/meta';
import PhotoGrid from '@/photo/PhotoGrid';
import { getUniqueCameras } from '@/services/postgres';
import { Metadata } from 'next';
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
import { pathForCamera } from '@/site/paths';
import {
PaginationParams,
getPaginationForSearchParams,
} from '@/site/pagination';
interface CameraProps {
params: { camera: string }
@ -24,14 +30,20 @@ export async function generateMetadata({
}: CameraProps): Promise<Metadata> {
const camera = getMakeModelFromCameraString(params.camera);
const photos = await getPhotosCached({ camera });
const [
photos,
count,
] = await Promise.all([
getPhotosCached({ camera, limit: GRID_THUMBNAILS_TO_SHOW_MAX }),
getPhotosCountCameraCached(camera),
]);
const {
url,
title,
description,
images,
} = generateMetaForCamera(camera, photos);
} = generateMetaForCamera(camera, photos, count);
return {
title,
@ -50,20 +62,35 @@ export async function generateMetadata({
};
}
export default async function Share({ params }: CameraProps) {
export default async function Share({
params,
searchParams,
}: CameraProps & PaginationParams) {
const cameraFromParams = getMakeModelFromCameraString(params.camera);
const photos = await getPhotosCached({ camera: cameraFromParams });
const { offset, limit } = getPaginationForSearchParams(searchParams);
const [
photos,
count,
] = await Promise.all([
getPhotosCached({ camera: cameraFromParams, limit }),
getPhotosCountCameraCached(cameraFromParams),
]);
const camera = cameraFromPhoto(photos[0], cameraFromParams);
const showMorePath = count > photos.length
? pathForCamera(camera, offset + 1)
: undefined;
return <>
<CameraShareModal {...{ camera, photos }} />
<CameraShareModal {...{ camera, photos, count }} />
<SiteGrid
key="Camera Grid"
contentMain={<div className="space-y-8 mt-4">
<CameraHeader camera={camera} photos={photos} />
<PhotoGrid photos={photos} camera={camera} />
<CameraHeader {...{ camera, photos, count }} />
<PhotoGrid {...{ photos, camera, showMorePath, animate: false }} />
</div>}
/>
</>;

View File

@ -1,7 +1,13 @@
import { getPhotosCached } from '@/cache';
import { getPhotosCached, getPhotosCountTagCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
import PhotoGrid from '@/photo/PhotoGrid';
import { getUniqueTags } from '@/services/postgres';
import {
PaginationParams,
getPaginationForSearchParams,
} from '@/site/pagination';
import { pathForTag } from '@/site/paths';
import { generateMetaForTag } from '@/tag';
import TagHeader from '@/tag/TagHeader';
import { Metadata } from 'next';
@ -20,14 +26,20 @@ export async function generateStaticParams() {
export async function generateMetadata({
params: { tag },
}: TagProps): Promise<Metadata> {
const photos = await getPhotosCached({ tag });
const [
photos,
count,
] = await Promise.all([
getPhotosCached({ tag, limit: GRID_THUMBNAILS_TO_SHOW_MAX }),
getPhotosCountTagCached(tag),
]);
const {
url,
title,
description,
images,
} = generateMetaForTag(tag, photos);
} = generateMetaForTag(tag, photos, count);
return {
title,
@ -46,15 +58,30 @@ export async function generateMetadata({
};
}
export default async function TagPage({ params: { tag } }:TagProps) {
const photos = await getPhotosCached({ tag });
export default async function TagPage({
params: { tag },
searchParams,
}:TagProps & PaginationParams) {
const { offset, limit } = getPaginationForSearchParams(searchParams);
const [
photos,
count,
] = await Promise.all([
getPhotosCached({ tag, limit }),
getPhotosCountTagCached(tag),
]);
const showMorePath = count > photos.length
? pathForTag(tag, offset + 1)
: undefined;
return (
<SiteGrid
key="Tag Grid"
contentMain={<div className="space-y-8 mt-4">
<TagHeader tag={tag} photos={photos} />
<PhotoGrid photos={photos} tag={tag} />
<TagHeader {...{ tag, photos, count }} />
<PhotoGrid {...{ photos, tag, showMorePath }} />
</div>}
/>
);

View File

@ -1,7 +1,13 @@
import { getPhotosCached } from '@/cache';
import { getPhotosCached, getPhotosCountTagCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
import PhotoGrid from '@/photo/PhotoGrid';
import { getUniqueTags } from '@/services/postgres';
import {
PaginationParams,
getPaginationForSearchParams,
} from '@/site/pagination';
import { pathForTag } from '@/site/paths';
import { generateMetaForTag } from '@/tag';
import TagHeader from '@/tag/TagHeader';
import TagShareModal from '@/tag/TagShareModal';
@ -21,14 +27,20 @@ export async function generateStaticParams() {
export async function generateMetadata({
params: { tag },
}: TagProps): Promise<Metadata> {
const photos = await getPhotosCached({ tag });
const [
photos,
count,
] = await Promise.all([
getPhotosCached({ tag, limit: GRID_THUMBNAILS_TO_SHOW_MAX }),
getPhotosCountTagCached(tag),
]);
const {
url,
title,
description,
images,
} = generateMetaForTag(tag, photos);
} = generateMetaForTag(tag, photos, count);
return {
title,
@ -49,17 +61,29 @@ export async function generateMetadata({
export default async function Share({
params: { tag },
}: {
params: { tag: string }
}) {
const photos = await getPhotosCached({ tag });
searchParams,
}: TagProps & PaginationParams) {
const { offset, limit } = getPaginationForSearchParams(searchParams);
const [
photos,
count,
] = await Promise.all([
getPhotosCached({ tag, limit }),
getPhotosCountTagCached(tag),
]);
const showMorePath = count > photos.length
? pathForTag(tag, offset + 1)
: undefined;
return <>
<TagShareModal {...{ tag, photos }} />
<TagShareModal {...{ tag, photos, count }} />
<SiteGrid
key="Tag Grid"
contentMain={<div className="space-y-8 mt-4">
<TagHeader tag={tag} photos={photos} />
<PhotoGrid photos={photos} tag={tag} animate={false} />
<TagHeader {...{ tag, photos }} />
<PhotoGrid {...{ photos, tag, showMorePath, animate: false }} />
</div>}
/>
</>;

21
src/cache/index.ts vendored
View File

@ -4,7 +4,9 @@ import {
getPhoto,
getPhotos,
getPhotosCount,
getPhotosCountCamera,
getPhotosCountIncludingHidden,
getPhotosCountTag,
getUniqueCameras,
getUniqueTags,
} from '@/services/postgres';
@ -15,7 +17,9 @@ import { AuthSession } from 'next-auth';
const TAG_PHOTOS = 'photos';
const TAG_PHOTOS_COUNT = 'photos-count';
const TAG_TAGS = 'tags';
const TAG_TAGS_COUNT = 'tags-count';
const TAG_CAMERAS = 'cameras';
const TAG_CAMERAS_COUNT = 'cameras-count';
const TAG_BLOB = 'blob';
// eslint-disable-next-line max-len
@ -100,6 +104,23 @@ export const getPhotosCountCached: typeof getPhotosCount = (...args) =>
}
)();
export const getPhotosCountTagCached: typeof getPhotosCountTag = (...args) =>
unstable_cache(
() => getPhotosCountTag(...args),
[TAG_PHOTOS, TAG_TAGS_COUNT], {
tags: [TAG_PHOTOS, TAG_TAGS_COUNT],
}
)();
// eslint-disable-next-line max-len
export const getPhotosCountCameraCached: typeof getPhotosCountCamera = (...args) =>
unstable_cache(
() => getPhotosCountCamera(...args),
[TAG_PHOTOS, TAG_CAMERAS_COUNT], {
tags: [TAG_PHOTOS, TAG_CAMERAS_COUNT],
}
)();
export const getPhotosCountIncludingHiddenCached: typeof getPhotosCount =
(...args) =>
unstable_cache(

View File

@ -9,20 +9,23 @@ export default function CameraHeader({
camera: cameraProp,
photos,
selectedPhoto,
count,
}: {
camera: Camera
photos: Photo[]
selectedPhoto?: Photo
count?: number
}) {
const camera = cameraFromPhoto(photos[0], cameraProp);
return (
<PhotoHeader
entity={<PhotoCamera {...{ camera }} />}
entityVerb="Photo"
entityDescription={descriptionForCameraPhotos(photos)}
entityDescription={descriptionForCameraPhotos(photos, undefined, count)}
photos={photos}
selectedPhoto={selectedPhoto}
sharePath={pathForCameraShare(camera)}
count={count}
/>
);
}

View File

@ -14,6 +14,7 @@ export default function CameraOGTile({
onLoad,
onFail,
retryTime,
count,
}: {
camera: Camera
photos: Photo[]
@ -22,11 +23,12 @@ export default function CameraOGTile({
onFail?: () => void
riseOnHover?: boolean
retryTime?: number
count?: number
}) {
return (
<OGTile {...{
title: titleForCamera(camera, photos),
description: descriptionForCameraPhotos(photos, true),
title: titleForCamera(camera, photos, count),
description: descriptionForCameraPhotos(photos, true, count),
path: pathForCamera(camera),
pathImageAbsolute: absolutePathForCameraImage(camera),
loadingState: loadingStateExternal,

View File

@ -7,9 +7,11 @@ import { Camera } from '.';
export default function CameraShareModal({
camera,
photos,
count,
}: {
camera: Camera
photos: Photo[]
count?: number
}) {
return (
<ShareModal
@ -17,7 +19,7 @@ export default function CameraShareModal({
pathShare={absolutePathForCamera(camera)}
pathClose={pathForCamera(camera)}
>
<CameraOGTile {...{ camera, photos }} />
<CameraOGTile {...{ camera, photos, count }} />
</ShareModal>
);
};

View File

@ -12,24 +12,27 @@ import {
export const titleForCamera = (
camera: Camera,
photos: Photo[],
explicitCount?: number,
) => [
'Shot on',
formatCameraText(cameraFromPhoto(photos[0], camera)),
photoQuantityText(photos),
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
export const descriptionForCameraPhotos = (
photos: Photo[],
dateBased?: boolean,
explicitCount?: number,
) =>
descriptionForPhotoSet(photos, undefined, dateBased);
descriptionForPhotoSet(photos, undefined, dateBased, explicitCount);
export const generateMetaForCamera = (
camera: Camera,
photos: Photo[]
photos: Photo[],
explicitCount?: number,
) => ({
url: absolutePathForCamera(camera),
title: titleForCamera(camera, photos),
description: descriptionForCameraPhotos(photos, true),
title: titleForCamera(camera, photos, explicitCount),
description: descriptionForCameraPhotos(photos, true, explicitCount),
images: absolutePathForCameraImage(camera),
});

View File

@ -3,6 +3,7 @@ import PhotoSmall from './PhotoSmall';
import { cc } from '@/utility/css';
import AnimateItems from '@/components/AnimateItems';
import { Camera } from '@/camera';
import MorePhotos from '@/components/MorePhotos';
export default function PhotoGrid({
photos,
@ -13,6 +14,7 @@ export default function PhotoGrid({
animate = true,
animateOnFirstLoadOnly,
staggerOnFirstLoadOnly = true,
showMorePath,
}: {
photos: Photo[]
selectedPhoto?: Photo
@ -22,28 +24,33 @@ export default function PhotoGrid({
animate?: boolean
animateOnFirstLoadOnly?: boolean
staggerOnFirstLoadOnly?: boolean
showMorePath?: string
}) {
return (
<AnimateItems
className={cc(
'grid gap-1',
'grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
'items-center',
)}
type={animate === false ? 'none' : undefined}
duration={fast ? 0.3 : undefined}
staggerDelay={0.075}
distanceOffset={40}
animateOnFirstLoadOnly={animateOnFirstLoadOnly}
staggerOnFirstLoadOnly={staggerOnFirstLoadOnly}
items={photos.map(photo =>
<PhotoSmall
key={photo.id}
photo={photo}
tag={tag}
camera={camera}
selected={photo.id === selectedPhoto?.id}
/>)}
/>
<div className="space-y-4">
<AnimateItems
className={cc(
'grid gap-1',
'grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
'items-center',
)}
type={animate === false ? 'none' : undefined}
duration={fast ? 0.3 : undefined}
staggerDelay={0.075}
distanceOffset={40}
animateOnFirstLoadOnly={animateOnFirstLoadOnly}
staggerOnFirstLoadOnly={staggerOnFirstLoadOnly}
items={photos.map(photo =>
<PhotoSmall
key={photo.id}
photo={photo}
tag={tag}
camera={camera}
selected={photo.id === selectedPhoto?.id}
/>)}
/>
{showMorePath &&
<MorePhotos path={showMorePath} />}
</div>
);
};

View File

@ -9,6 +9,7 @@ export default function PhotoHeader({
photos,
selectedPhoto,
sharePath,
count,
}: {
entity: JSX.Element
entityVerb: string
@ -16,6 +17,7 @@ export default function PhotoHeader({
photos: Photo[]
selectedPhoto?: Photo
sharePath: string
count?: number
}) {
const { start, end } = dateRangeForPhotos(photos);
@ -35,7 +37,8 @@ export default function PhotoHeader({
'sm:col-span-2 md:col-span-1 lg:col-span-2',
)}>
{selectedPhotoIndex !== undefined
? `${entityVerb} ${selectedPhotoIndex + 1} of ${photos.length}`
// eslint-disable-next-line max-len
? `${entityVerb} ${selectedPhotoIndex + 1} of ${count ?? photos.length}`
: entityDescription}
{selectedPhotoIndex === undefined &&
<ShareButton path={sharePath} dim />}

View File

@ -130,18 +130,6 @@ export const getNextPhoto = (photo: Photo, photos: Photo[]) => {
: undefined;
};
export const getPhotosLimitForQuery = (
query?: string,
photosPerRequest = 24,
) => {
const offsetInt = parseInt(query ?? '0');
const offset = (Number.isNaN(offsetInt) ? 0 : offsetInt);
return {
offset,
limit: photosPerRequest + offset * photosPerRequest,
};
};
export const generateOgImageMetaForPhotos = (photos: Photo[]): Metadata => {
if (photos.length > 0) {
return {
@ -169,23 +157,24 @@ export const translatePhotoId = (id: string) =>
export const titleForPhoto = (photo: Photo) =>
photo.title || 'Untitled';
const labelForPhotos = (photos: Photo[]) =>
photos.length === 1 ? 'Photo' : 'Photos';
const photoLabelForCount = (count: number) =>
count === 1 ? 'Photo' : 'Photos';
export const photoQuantityText = (photos: Photo[]) =>
`(${photos.length} ${labelForPhotos(photos)})`;
export const photoQuantityText = (count: number) =>
`(${count} ${photoLabelForCount(count)})`;
export const descriptionForPhotoSet = (
photos:Photo[],
descriptor?: string,
dateBased?: boolean,
explicitCount?: number,
) =>
dateBased
? dateRangeForPhotos(photos).description.toUpperCase()
: [
photos.length,
explicitCount ?? photos.length,
descriptor,
labelForPhotos(photos),
photoLabelForCount(explicitCount ?? photos.length),
].join(' ');
export const dateRangeForPhotos = (photos: Photo[]) => {

View File

@ -239,6 +239,20 @@ const sqlGetPhotosCountIncludingHidden = async () => sql`
SELECT COUNT(*) FROM photos
`.then(({ rows }) => parseInt(rows[0].count, 10));
const sqlGetPhotosCountTag = async (tag: string) => sql`
SELECT COUNT(*) FROM photos
WHERE ${tag}=ANY(tags) AND
hidden IS NOT TRUE
`.then(({ rows }) => parseInt(rows[0].count, 10));
const sqlGetPhotosCountCamera = async (camera: Camera) => sql`
SELECT COUNT(*) FROM photos
WHERE
LOWER(make)=${parameterize(camera.make)} AND
LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model)} AND
hidden IS NOT TRUE
`.then(({ rows }) => parseInt(rows[0].count, 10));
const sqlGetUniqueTags = async () => sql`
SELECT DISTINCT unnest(tags) as tag FROM photos
WHERE hidden IS NOT TRUE
@ -337,7 +351,12 @@ export const getPhoto = async (id: string): Promise<Photo | undefined> => {
.then(photos => photos.length > 0 ? photos[0] : undefined);
};
export const getPhotosCount = () => safelyQueryPhotos(sqlGetPhotosCount);
export const getPhotosCount = () =>
safelyQueryPhotos(sqlGetPhotosCount);
export const getPhotosCountTag = (tag: string) =>
safelyQueryPhotos(() => sqlGetPhotosCountTag(tag));
export const getPhotosCountCamera = (camera: Camera) =>
safelyQueryPhotos(() => sqlGetPhotosCountCamera(camera));
export const getPhotosCountIncludingHidden = () =>
safelyQueryPhotos(sqlGetPhotosCountIncludingHidden);

17
src/site/pagination.ts Normal file
View File

@ -0,0 +1,17 @@
type PaginationSearchParams = { next: string };
export interface PaginationParams {
searchParams?: PaginationSearchParams
}
export const getPaginationForSearchParams = (
query?: PaginationSearchParams,
limitPerOffset = 24,
) => {
const offsetInt = parseInt(query?.next ?? '0');
const offset = (Number.isNaN(offsetInt) ? 0 : offsetInt);
return {
offset,
limit: limitPerOffset + offset * limitPerOffset,
};
};

View File

@ -72,14 +72,20 @@ export const pathForPhotoShare = (
export const pathForPhotoEdit = (photo: PhotoOrPhotoId) =>
`${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/edit`;
export const pathForTag = (tag: string) =>
`${PREFIX_TAG}/${tag}`;
export const pathForTag = (tag: string, next?: number) =>
pathWithNext(
`${PREFIX_TAG}/${tag}`,
next,
);
export const pathForTagShare = (tag: string) =>
`${pathForTag(tag)}/${SHARE}`;
export const pathForCamera = ({ make, model }: Camera) =>
`${PREFIX_CAMERA}/${createCameraKey(make, model)}`;
export const pathForCamera = ({ make, model }: Camera, next?: number) =>
pathWithNext(
`${PREFIX_CAMERA}/${createCameraKey(make, model)}`,
next,
);
export const pathForCameraShare = (camera: Camera) =>
`${pathForCamera(camera)}/${SHARE}`;

View File

@ -8,19 +8,22 @@ export default function TagHeader({
tag,
photos,
selectedPhoto,
count,
}: {
tag: string
photos: Photo[]
selectedPhoto?: Photo
count?: number
}) {
return (
<PhotoHeader
entity={<PhotoTag tag={tag} />}
entityVerb="Tagged"
entityDescription={descriptionForTaggedPhotos(photos)}
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
photos={photos}
selectedPhoto={selectedPhoto}
sharePath={pathForTagShare(tag)}
count={count}
/>
);
}

View File

@ -13,6 +13,7 @@ export default function TagOGTile({
onLoad,
onFail,
retryTime,
count,
}: {
tag: string
photos: Photo[]
@ -21,11 +22,12 @@ export default function TagOGTile({
onFail?: () => void
riseOnHover?: boolean
retryTime?: number
count?: number
}) {
return (
<OGTile {...{
title: titleForTag(tag, photos),
description: descriptionForTaggedPhotos(photos, true),
description: descriptionForTaggedPhotos(photos, true, count),
path: pathForTag(tag),
pathImageAbsolute: absolutePathForTagImage(tag),
loadingState: loadingStateExternal,

View File

@ -6,9 +6,11 @@ import TagOGTile from './TagOGTile';
export default function TagShareModal({
tag,
photos,
count,
}: {
tag: string
photos: Photo[]
count?: number
}) {
return (
<ShareModal
@ -16,7 +18,7 @@ export default function TagShareModal({
pathShare={absolutePathForTag(tag)}
pathClose={pathForTag(tag)}
>
<TagOGTile tag={tag} photos={photos} />
<TagOGTile {...{ tag, photos, count }} />
</ShareModal>
);
};

View File

@ -2,20 +2,29 @@ import { Photo, descriptionForPhotoSet, photoQuantityText } from '@/photo';
import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths';
import { capitalizeWords } from '@/utility/string';
export const titleForTag = (tag: string, photos:Photo[]) => [
export const titleForTag = (
tag: string,
photos:Photo[],
explicitCount?: number,
) => [
capitalizeWords(tag.replaceAll('-', ' ')),
photoQuantityText(photos),
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
export const descriptionForTaggedPhotos = (
photos: Photo[],
dateBased?: boolean,
explicitCount?: number,
) =>
descriptionForPhotoSet(photos, 'tagged', dateBased);
descriptionForPhotoSet(photos, 'tagged', dateBased, explicitCount);
export const generateMetaForTag = (tag: string, photos: Photo[]) => ({
export const generateMetaForTag = (
tag: string,
photos: Photo[],
explicitCount?: number,
) => ({
url: absolutePathForTag(tag),
title: titleForTag(tag, photos),
title: titleForTag(tag, photos, explicitCount),
description: descriptionForTaggedPhotos(photos, true),
images: absolutePathForTagImage(tag),
});