Paginate camera and tag views
This commit is contained in:
parent
ee841518ec
commit
e93e23f428
@ -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,
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 { offset, limit } = getPaginationForSearchParams(searchParams);
|
||||
|
||||
const photos = await getPhotosCached({ camera });
|
||||
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>}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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>}
|
||||
/>
|
||||
</>;
|
||||
|
||||
@ -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>}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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
21
src/cache/index.ts
vendored
@ -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(
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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),
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 />}
|
||||
|
||||
@ -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[]) => {
|
||||
|
||||
@ -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
17
src/site/pagination.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@ -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}`;
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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),
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user