Paginate camera and tag views
This commit is contained in:
parent
ee841518ec
commit
e93e23f428
@ -19,7 +19,7 @@ import {
|
|||||||
pathForPhoto,
|
pathForPhoto,
|
||||||
pathForPhotoEdit,
|
pathForPhotoEdit,
|
||||||
} from '@/site/paths';
|
} from '@/site/paths';
|
||||||
import { getPhotosLimitForQuery, titleForPhoto } from '@/photo';
|
import { titleForPhoto } from '@/photo';
|
||||||
import MorePhotos from '@/components/MorePhotos';
|
import MorePhotos from '@/components/MorePhotos';
|
||||||
import {
|
import {
|
||||||
getBlobPhotoUrlsCached,
|
getBlobPhotoUrlsCached,
|
||||||
@ -29,17 +29,17 @@ import {
|
|||||||
} from '@/cache';
|
} from '@/cache';
|
||||||
import { AiOutlineEyeInvisible } from 'react-icons/ai';
|
import { AiOutlineEyeInvisible } from 'react-icons/ai';
|
||||||
import { BiTrash } from 'react-icons/bi';
|
import { BiTrash } from 'react-icons/bi';
|
||||||
|
import {
|
||||||
|
PaginationParams,
|
||||||
|
getPaginationForSearchParams,
|
||||||
|
} from '@/site/pagination';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
const DEBUG_PHOTO_BLOBS = false;
|
const DEBUG_PHOTO_BLOBS = false;
|
||||||
|
|
||||||
export default async function AdminPage({
|
export default async function AdminPage({ searchParams }: PaginationParams) {
|
||||||
searchParams,
|
const { offset, limit } = getPaginationForSearchParams(searchParams);
|
||||||
}: {
|
|
||||||
searchParams: { next: string };
|
|
||||||
}) {
|
|
||||||
const { offset, limit } = getPhotosLimitForQuery(searchParams.next);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
|
|||||||
@ -5,9 +5,8 @@ import {
|
|||||||
getUniqueTagsCached,
|
getUniqueTagsCached,
|
||||||
} from '@/cache';
|
} from '@/cache';
|
||||||
import HeaderList from '@/components/HeaderList';
|
import HeaderList from '@/components/HeaderList';
|
||||||
import MorePhotos from '@/components/MorePhotos';
|
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo';
|
import { generateOgImageMetaForPhotos } from '@/photo';
|
||||||
import PhotoCamera from '@/camera/PhotoCamera';
|
import PhotoCamera from '@/camera/PhotoCamera';
|
||||||
import PhotoGrid from '@/photo/PhotoGrid';
|
import PhotoGrid from '@/photo/PhotoGrid';
|
||||||
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||||
@ -17,6 +16,10 @@ import PhotoTag from '@/tag/PhotoTag';
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { FaTag } from 'react-icons/fa';
|
import { FaTag } from 'react-icons/fa';
|
||||||
import { IoMdCamera } from 'react-icons/io';
|
import { IoMdCamera } from 'react-icons/io';
|
||||||
|
import {
|
||||||
|
PaginationParams,
|
||||||
|
getPaginationForSearchParams,
|
||||||
|
} from '@/site/pagination';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
@ -25,12 +28,8 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
return generateOgImageMetaForPhotos(photos);
|
return generateOgImageMetaForPhotos(photos);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function GridPage({
|
export default async function GridPage({ searchParams }: PaginationParams) {
|
||||||
searchParams,
|
const { offset, limit } = getPaginationForSearchParams(searchParams);
|
||||||
}: {
|
|
||||||
searchParams: { next: string };
|
|
||||||
}) {
|
|
||||||
const { offset, limit } = getPhotosLimitForQuery(searchParams.next);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
@ -44,16 +43,14 @@ export default async function GridPage({
|
|||||||
getUniqueCamerasCached(),
|
getUniqueCamerasCached(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const showMorePhotos = count > photos.length;
|
const showMorePath = count > photos.length
|
||||||
|
? pathForGrid(offset + 1)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
photos.length > 0
|
photos.length > 0
|
||||||
? <SiteGrid
|
? <SiteGrid
|
||||||
contentMain={<div className="space-y-4">
|
contentMain={<PhotoGrid {...{ photos, showMorePath }} />}
|
||||||
<PhotoGrid photos={photos} />
|
|
||||||
{showMorePhotos &&
|
|
||||||
<MorePhotos path={pathForGrid(offset + 1)} />}
|
|
||||||
</div>}
|
|
||||||
contentSide={<div className="sticky top-4 space-y-4">
|
contentSide={<div className="sticky top-4 space-y-4">
|
||||||
{tags.length > 0 && <HeaderList
|
{tags.length > 0 && <HeaderList
|
||||||
title='Tags'
|
title='Tags'
|
||||||
|
|||||||
@ -1,17 +1,16 @@
|
|||||||
import { getPhotosCached, getPhotosCountCached } from '@/cache';
|
import { getPhotosCached, getPhotosCountCached } from '@/cache';
|
||||||
import MorePhotos from '@/components/MorePhotos';
|
import MorePhotos from '@/components/MorePhotos';
|
||||||
import { getPhotosLimitForQuery } from '@/photo';
|
|
||||||
import StaggeredOgPhotos from '@/photo/StaggeredOgPhotos';
|
import StaggeredOgPhotos from '@/photo/StaggeredOgPhotos';
|
||||||
|
import {
|
||||||
|
PaginationParams,
|
||||||
|
getPaginationForSearchParams,
|
||||||
|
} from '@/site/pagination';
|
||||||
import { pathForOg } from '@/site/paths';
|
import { pathForOg } from '@/site/paths';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
export default async function GridPage({
|
export default async function GridPage({ searchParams }: PaginationParams) {
|
||||||
searchParams,
|
const { offset, limit } = getPaginationForSearchParams(searchParams);
|
||||||
}: {
|
|
||||||
searchParams: { next: string };
|
|
||||||
}) {
|
|
||||||
const { offset, limit } = getPhotosLimitForQuery(searchParams.next);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
|
|||||||
@ -2,9 +2,13 @@ import { getPhotosCached, getPhotosCountCached } from '@/cache';
|
|||||||
import AnimateItems from '@/components/AnimateItems';
|
import AnimateItems from '@/components/AnimateItems';
|
||||||
import MorePhotos from '@/components/MorePhotos';
|
import MorePhotos from '@/components/MorePhotos';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo';
|
import { generateOgImageMetaForPhotos } from '@/photo';
|
||||||
import PhotoLarge from '@/photo/PhotoLarge';
|
import PhotoLarge from '@/photo/PhotoLarge';
|
||||||
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||||
|
import {
|
||||||
|
PaginationParams,
|
||||||
|
getPaginationForSearchParams,
|
||||||
|
} from '@/site/pagination';
|
||||||
import { pathForRoot } from '@/site/paths';
|
import { pathForRoot } from '@/site/paths';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
@ -15,12 +19,8 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
return generateOgImageMetaForPhotos(photos);
|
return generateOgImageMetaForPhotos(photos);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function HomePage({
|
export default async function HomePage({ searchParams }: PaginationParams) {
|
||||||
searchParams,
|
const { offset, limit } = getPaginationForSearchParams(searchParams, 12);
|
||||||
}: {
|
|
||||||
searchParams: { next: string };
|
|
||||||
}) {
|
|
||||||
const { offset, limit } = getPhotosLimitForQuery(searchParams.next, 12);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { getPhotosCached } from '@/cache';
|
import { getPhotosCached, getPhotosCountCameraCached } from '@/cache';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import CameraHeader from '@/camera/CameraHeader';
|
import CameraHeader from '@/camera/CameraHeader';
|
||||||
import { getMakeModelFromCameraString } from '@/camera';
|
import { getMakeModelFromCameraString } from '@/camera';
|
||||||
@ -6,9 +6,15 @@ import PhotoGrid from '@/photo/PhotoGrid';
|
|||||||
import { getUniqueCameras } from '@/services/postgres';
|
import { getUniqueCameras } from '@/services/postgres';
|
||||||
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 { pathForCamera } from '@/site/paths';
|
||||||
|
import {
|
||||||
|
PaginationParams,
|
||||||
|
getPaginationForSearchParams,
|
||||||
|
} from '@/site/pagination';
|
||||||
|
|
||||||
interface CameraProps {
|
interface CameraProps {
|
||||||
params: { camera: string }
|
params: { camera: string },
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
@ -22,14 +28,21 @@ export async function generateMetadata({
|
|||||||
params,
|
params,
|
||||||
}: CameraProps): Promise<Metadata> {
|
}: CameraProps): Promise<Metadata> {
|
||||||
const camera = getMakeModelFromCameraString(params.camera);
|
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 {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForCamera(camera, photos);
|
} = generateMetaForCamera(camera, photos, count);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
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 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 (
|
return (
|
||||||
<SiteGrid
|
<SiteGrid
|
||||||
key="Camera Grid"
|
key="Camera Grid"
|
||||||
contentMain={<div className="space-y-8 mt-4">
|
contentMain={<div className="space-y-8 mt-4">
|
||||||
<CameraHeader camera={camera} photos={photos} />
|
<CameraHeader {...{ camera, photos, count }} />
|
||||||
<PhotoGrid photos={photos} camera={camera} />
|
<PhotoGrid {...{ photos, camera, showMorePath }} />
|
||||||
</div>}
|
</div>}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { getPhotosCached } from '@/cache';
|
import { getPhotosCached, getPhotosCountCameraCached } from '@/cache';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import { cameraFromPhoto, getMakeModelFromCameraString } from '@/camera';
|
import { cameraFromPhoto, getMakeModelFromCameraString } from '@/camera';
|
||||||
import CameraHeader from '@/camera/CameraHeader';
|
import CameraHeader from '@/camera/CameraHeader';
|
||||||
@ -7,6 +7,12 @@ import { generateMetaForCamera } from '@/camera/meta';
|
|||||||
import PhotoGrid from '@/photo/PhotoGrid';
|
import PhotoGrid from '@/photo/PhotoGrid';
|
||||||
import { getUniqueCameras } from '@/services/postgres';
|
import { getUniqueCameras } from '@/services/postgres';
|
||||||
import { Metadata } from 'next';
|
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 {
|
interface CameraProps {
|
||||||
params: { camera: string }
|
params: { camera: string }
|
||||||
@ -24,14 +30,20 @@ export async function generateMetadata({
|
|||||||
}: CameraProps): Promise<Metadata> {
|
}: CameraProps): Promise<Metadata> {
|
||||||
const camera = getMakeModelFromCameraString(params.camera);
|
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 {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForCamera(camera, photos);
|
} = generateMetaForCamera(camera, photos, count);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
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 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 camera = cameraFromPhoto(photos[0], cameraFromParams);
|
||||||
|
|
||||||
|
const showMorePath = count > photos.length
|
||||||
|
? pathForCamera(camera, offset + 1)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<CameraShareModal {...{ camera, photos }} />
|
<CameraShareModal {...{ camera, photos, count }} />
|
||||||
<SiteGrid
|
<SiteGrid
|
||||||
key="Camera Grid"
|
key="Camera Grid"
|
||||||
contentMain={<div className="space-y-8 mt-4">
|
contentMain={<div className="space-y-8 mt-4">
|
||||||
<CameraHeader camera={camera} photos={photos} />
|
<CameraHeader {...{ camera, photos, count }} />
|
||||||
<PhotoGrid photos={photos} camera={camera} />
|
<PhotoGrid {...{ photos, camera, showMorePath, animate: false }} />
|
||||||
</div>}
|
</div>}
|
||||||
/>
|
/>
|
||||||
</>;
|
</>;
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { getPhotosCached } from '@/cache';
|
import { getPhotosCached, getPhotosCountTagCached } from '@/cache';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
|
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
||||||
import PhotoGrid from '@/photo/PhotoGrid';
|
import PhotoGrid from '@/photo/PhotoGrid';
|
||||||
import { getUniqueTags } from '@/services/postgres';
|
import { getUniqueTags } from '@/services/postgres';
|
||||||
|
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 TagHeader from '@/tag/TagHeader';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
@ -20,14 +26,20 @@ export async function generateStaticParams() {
|
|||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params: { tag },
|
params: { tag },
|
||||||
}: TagProps): Promise<Metadata> {
|
}: 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 {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForTag(tag, photos);
|
} = generateMetaForTag(tag, photos, count);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@ -46,15 +58,30 @@ export async function generateMetadata({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function TagPage({ params: { tag } }:TagProps) {
|
export default async function TagPage({
|
||||||
const photos = await getPhotosCached({ tag });
|
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 (
|
return (
|
||||||
<SiteGrid
|
<SiteGrid
|
||||||
key="Tag Grid"
|
key="Tag Grid"
|
||||||
contentMain={<div className="space-y-8 mt-4">
|
contentMain={<div className="space-y-8 mt-4">
|
||||||
<TagHeader tag={tag} photos={photos} />
|
<TagHeader {...{ tag, photos, count }} />
|
||||||
<PhotoGrid photos={photos} tag={tag} />
|
<PhotoGrid {...{ photos, tag, showMorePath }} />
|
||||||
</div>}
|
</div>}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { getPhotosCached } from '@/cache';
|
import { getPhotosCached, getPhotosCountTagCached } from '@/cache';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
|
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
||||||
import PhotoGrid from '@/photo/PhotoGrid';
|
import PhotoGrid from '@/photo/PhotoGrid';
|
||||||
import { getUniqueTags } from '@/services/postgres';
|
import { getUniqueTags } from '@/services/postgres';
|
||||||
|
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 TagHeader from '@/tag/TagHeader';
|
||||||
import TagShareModal from '@/tag/TagShareModal';
|
import TagShareModal from '@/tag/TagShareModal';
|
||||||
@ -21,14 +27,20 @@ export async function generateStaticParams() {
|
|||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params: { tag },
|
params: { tag },
|
||||||
}: TagProps): Promise<Metadata> {
|
}: 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 {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForTag(tag, photos);
|
} = generateMetaForTag(tag, photos, count);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@ -49,17 +61,29 @@ export async function generateMetadata({
|
|||||||
|
|
||||||
export default async function Share({
|
export default async function Share({
|
||||||
params: { tag },
|
params: { tag },
|
||||||
}: {
|
searchParams,
|
||||||
params: { tag: string }
|
}: TagProps & PaginationParams) {
|
||||||
}) {
|
const { offset, limit } = getPaginationForSearchParams(searchParams);
|
||||||
const photos = await getPhotosCached({ tag });
|
|
||||||
|
const [
|
||||||
|
photos,
|
||||||
|
count,
|
||||||
|
] = await Promise.all([
|
||||||
|
getPhotosCached({ tag, limit }),
|
||||||
|
getPhotosCountTagCached(tag),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const showMorePath = count > photos.length
|
||||||
|
? pathForTag(tag, offset + 1)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<TagShareModal {...{ tag, photos }} />
|
<TagShareModal {...{ tag, photos, count }} />
|
||||||
<SiteGrid
|
<SiteGrid
|
||||||
key="Tag Grid"
|
key="Tag Grid"
|
||||||
contentMain={<div className="space-y-8 mt-4">
|
contentMain={<div className="space-y-8 mt-4">
|
||||||
<TagHeader tag={tag} photos={photos} />
|
<TagHeader {...{ tag, photos }} />
|
||||||
<PhotoGrid photos={photos} tag={tag} animate={false} />
|
<PhotoGrid {...{ photos, tag, showMorePath, animate: false }} />
|
||||||
</div>}
|
</div>}
|
||||||
/>
|
/>
|
||||||
</>;
|
</>;
|
||||||
|
|||||||
21
src/cache/index.ts
vendored
21
src/cache/index.ts
vendored
@ -4,7 +4,9 @@ import {
|
|||||||
getPhoto,
|
getPhoto,
|
||||||
getPhotos,
|
getPhotos,
|
||||||
getPhotosCount,
|
getPhotosCount,
|
||||||
|
getPhotosCountCamera,
|
||||||
getPhotosCountIncludingHidden,
|
getPhotosCountIncludingHidden,
|
||||||
|
getPhotosCountTag,
|
||||||
getUniqueCameras,
|
getUniqueCameras,
|
||||||
getUniqueTags,
|
getUniqueTags,
|
||||||
} from '@/services/postgres';
|
} from '@/services/postgres';
|
||||||
@ -15,7 +17,9 @@ import { AuthSession } from 'next-auth';
|
|||||||
const TAG_PHOTOS = 'photos';
|
const TAG_PHOTOS = 'photos';
|
||||||
const TAG_PHOTOS_COUNT = 'photos-count';
|
const TAG_PHOTOS_COUNT = 'photos-count';
|
||||||
const TAG_TAGS = 'tags';
|
const TAG_TAGS = 'tags';
|
||||||
|
const TAG_TAGS_COUNT = 'tags-count';
|
||||||
const TAG_CAMERAS = 'cameras';
|
const TAG_CAMERAS = 'cameras';
|
||||||
|
const TAG_CAMERAS_COUNT = 'cameras-count';
|
||||||
const TAG_BLOB = 'blob';
|
const TAG_BLOB = 'blob';
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// 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 =
|
export const getPhotosCountIncludingHiddenCached: typeof getPhotosCount =
|
||||||
(...args) =>
|
(...args) =>
|
||||||
unstable_cache(
|
unstable_cache(
|
||||||
|
|||||||
@ -9,20 +9,23 @@ export default function CameraHeader({
|
|||||||
camera: cameraProp,
|
camera: cameraProp,
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
|
count,
|
||||||
}: {
|
}: {
|
||||||
camera: Camera
|
camera: Camera
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
selectedPhoto?: Photo
|
selectedPhoto?: Photo
|
||||||
|
count?: number
|
||||||
}) {
|
}) {
|
||||||
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)}
|
entityDescription={descriptionForCameraPhotos(photos, undefined, count)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
sharePath={pathForCameraShare(camera)}
|
sharePath={pathForCameraShare(camera)}
|
||||||
|
count={count}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export default function CameraOGTile({
|
|||||||
onLoad,
|
onLoad,
|
||||||
onFail,
|
onFail,
|
||||||
retryTime,
|
retryTime,
|
||||||
|
count,
|
||||||
}: {
|
}: {
|
||||||
camera: Camera
|
camera: Camera
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
@ -22,11 +23,12 @@ export default function CameraOGTile({
|
|||||||
onFail?: () => void
|
onFail?: () => void
|
||||||
riseOnHover?: boolean
|
riseOnHover?: boolean
|
||||||
retryTime?: number
|
retryTime?: number
|
||||||
|
count?: number
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<OGTile {...{
|
<OGTile {...{
|
||||||
title: titleForCamera(camera, photos),
|
title: titleForCamera(camera, photos, count),
|
||||||
description: descriptionForCameraPhotos(photos, true),
|
description: descriptionForCameraPhotos(photos, true, count),
|
||||||
path: pathForCamera(camera),
|
path: pathForCamera(camera),
|
||||||
pathImageAbsolute: absolutePathForCameraImage(camera),
|
pathImageAbsolute: absolutePathForCameraImage(camera),
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
|
|||||||
@ -7,9 +7,11 @@ import { Camera } from '.';
|
|||||||
export default function CameraShareModal({
|
export default function CameraShareModal({
|
||||||
camera,
|
camera,
|
||||||
photos,
|
photos,
|
||||||
|
count,
|
||||||
}: {
|
}: {
|
||||||
camera: Camera
|
camera: Camera
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
|
count?: number
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
@ -17,7 +19,7 @@ export default function CameraShareModal({
|
|||||||
pathShare={absolutePathForCamera(camera)}
|
pathShare={absolutePathForCamera(camera)}
|
||||||
pathClose={pathForCamera(camera)}
|
pathClose={pathForCamera(camera)}
|
||||||
>
|
>
|
||||||
<CameraOGTile {...{ camera, photos }} />
|
<CameraOGTile {...{ camera, photos, count }} />
|
||||||
</ShareModal>
|
</ShareModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,24 +12,27 @@ import {
|
|||||||
export const titleForCamera = (
|
export const titleForCamera = (
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
explicitCount?: number,
|
||||||
) => [
|
) => [
|
||||||
'Shot on',
|
'Shot on',
|
||||||
formatCameraText(cameraFromPhoto(photos[0], camera)),
|
formatCameraText(cameraFromPhoto(photos[0], camera)),
|
||||||
photoQuantityText(photos),
|
photoQuantityText(explicitCount ?? photos.length),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const descriptionForCameraPhotos = (
|
export const descriptionForCameraPhotos = (
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
|
explicitCount?: number,
|
||||||
) =>
|
) =>
|
||||||
descriptionForPhotoSet(photos, undefined, dateBased);
|
descriptionForPhotoSet(photos, undefined, dateBased, explicitCount);
|
||||||
|
|
||||||
export const generateMetaForCamera = (
|
export const generateMetaForCamera = (
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
photos: Photo[]
|
photos: Photo[],
|
||||||
|
explicitCount?: number,
|
||||||
) => ({
|
) => ({
|
||||||
url: absolutePathForCamera(camera),
|
url: absolutePathForCamera(camera),
|
||||||
title: titleForCamera(camera, photos),
|
title: titleForCamera(camera, photos, explicitCount),
|
||||||
description: descriptionForCameraPhotos(photos, true),
|
description: descriptionForCameraPhotos(photos, true, explicitCount),
|
||||||
images: absolutePathForCameraImage(camera),
|
images: absolutePathForCameraImage(camera),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import PhotoSmall from './PhotoSmall';
|
|||||||
import { cc } from '@/utility/css';
|
import { cc } from '@/utility/css';
|
||||||
import AnimateItems from '@/components/AnimateItems';
|
import AnimateItems from '@/components/AnimateItems';
|
||||||
import { Camera } from '@/camera';
|
import { Camera } from '@/camera';
|
||||||
|
import MorePhotos from '@/components/MorePhotos';
|
||||||
|
|
||||||
export default function PhotoGrid({
|
export default function PhotoGrid({
|
||||||
photos,
|
photos,
|
||||||
@ -13,6 +14,7 @@ export default function PhotoGrid({
|
|||||||
animate = true,
|
animate = true,
|
||||||
animateOnFirstLoadOnly,
|
animateOnFirstLoadOnly,
|
||||||
staggerOnFirstLoadOnly = true,
|
staggerOnFirstLoadOnly = true,
|
||||||
|
showMorePath,
|
||||||
}: {
|
}: {
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
selectedPhoto?: Photo
|
selectedPhoto?: Photo
|
||||||
@ -22,28 +24,33 @@ export default function PhotoGrid({
|
|||||||
animate?: boolean
|
animate?: boolean
|
||||||
animateOnFirstLoadOnly?: boolean
|
animateOnFirstLoadOnly?: boolean
|
||||||
staggerOnFirstLoadOnly?: boolean
|
staggerOnFirstLoadOnly?: boolean
|
||||||
|
showMorePath?: string
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<AnimateItems
|
<div className="space-y-4">
|
||||||
className={cc(
|
<AnimateItems
|
||||||
'grid gap-1',
|
className={cc(
|
||||||
'grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
|
'grid gap-1',
|
||||||
'items-center',
|
'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}
|
type={animate === false ? 'none' : undefined}
|
||||||
staggerDelay={0.075}
|
duration={fast ? 0.3 : undefined}
|
||||||
distanceOffset={40}
|
staggerDelay={0.075}
|
||||||
animateOnFirstLoadOnly={animateOnFirstLoadOnly}
|
distanceOffset={40}
|
||||||
staggerOnFirstLoadOnly={staggerOnFirstLoadOnly}
|
animateOnFirstLoadOnly={animateOnFirstLoadOnly}
|
||||||
items={photos.map(photo =>
|
staggerOnFirstLoadOnly={staggerOnFirstLoadOnly}
|
||||||
<PhotoSmall
|
items={photos.map(photo =>
|
||||||
key={photo.id}
|
<PhotoSmall
|
||||||
photo={photo}
|
key={photo.id}
|
||||||
tag={tag}
|
photo={photo}
|
||||||
camera={camera}
|
tag={tag}
|
||||||
selected={photo.id === selectedPhoto?.id}
|
camera={camera}
|
||||||
/>)}
|
selected={photo.id === selectedPhoto?.id}
|
||||||
/>
|
/>)}
|
||||||
|
/>
|
||||||
|
{showMorePath &&
|
||||||
|
<MorePhotos path={showMorePath} />}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export default function PhotoHeader({
|
|||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
sharePath,
|
sharePath,
|
||||||
|
count,
|
||||||
}: {
|
}: {
|
||||||
entity: JSX.Element
|
entity: JSX.Element
|
||||||
entityVerb: string
|
entityVerb: string
|
||||||
@ -16,6 +17,7 @@ export default function PhotoHeader({
|
|||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
selectedPhoto?: Photo
|
selectedPhoto?: Photo
|
||||||
sharePath: string
|
sharePath: string
|
||||||
|
count?: number
|
||||||
}) {
|
}) {
|
||||||
const { start, end } = dateRangeForPhotos(photos);
|
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',
|
'sm:col-span-2 md:col-span-1 lg:col-span-2',
|
||||||
)}>
|
)}>
|
||||||
{selectedPhotoIndex !== undefined
|
{selectedPhotoIndex !== undefined
|
||||||
? `${entityVerb} ${selectedPhotoIndex + 1} of ${photos.length}`
|
// eslint-disable-next-line max-len
|
||||||
|
? `${entityVerb} ${selectedPhotoIndex + 1} of ${count ?? photos.length}`
|
||||||
: entityDescription}
|
: entityDescription}
|
||||||
{selectedPhotoIndex === undefined &&
|
{selectedPhotoIndex === undefined &&
|
||||||
<ShareButton path={sharePath} dim />}
|
<ShareButton path={sharePath} dim />}
|
||||||
|
|||||||
@ -130,18 +130,6 @@ export const getNextPhoto = (photo: Photo, photos: Photo[]) => {
|
|||||||
: undefined;
|
: 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 => {
|
export const generateOgImageMetaForPhotos = (photos: Photo[]): Metadata => {
|
||||||
if (photos.length > 0) {
|
if (photos.length > 0) {
|
||||||
return {
|
return {
|
||||||
@ -169,23 +157,24 @@ export const translatePhotoId = (id: string) =>
|
|||||||
export const titleForPhoto = (photo: Photo) =>
|
export const titleForPhoto = (photo: Photo) =>
|
||||||
photo.title || 'Untitled';
|
photo.title || 'Untitled';
|
||||||
|
|
||||||
const labelForPhotos = (photos: Photo[]) =>
|
const photoLabelForCount = (count: number) =>
|
||||||
photos.length === 1 ? 'Photo' : 'Photos';
|
count === 1 ? 'Photo' : 'Photos';
|
||||||
|
|
||||||
export const photoQuantityText = (photos: Photo[]) =>
|
export const photoQuantityText = (count: number) =>
|
||||||
`(${photos.length} ${labelForPhotos(photos)})`;
|
`(${count} ${photoLabelForCount(count)})`;
|
||||||
|
|
||||||
export const descriptionForPhotoSet = (
|
export const descriptionForPhotoSet = (
|
||||||
photos:Photo[],
|
photos:Photo[],
|
||||||
descriptor?: string,
|
descriptor?: string,
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
|
explicitCount?: number,
|
||||||
) =>
|
) =>
|
||||||
dateBased
|
dateBased
|
||||||
? dateRangeForPhotos(photos).description.toUpperCase()
|
? dateRangeForPhotos(photos).description.toUpperCase()
|
||||||
: [
|
: [
|
||||||
photos.length,
|
explicitCount ?? photos.length,
|
||||||
descriptor,
|
descriptor,
|
||||||
labelForPhotos(photos),
|
photoLabelForCount(explicitCount ?? photos.length),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const dateRangeForPhotos = (photos: Photo[]) => {
|
export const dateRangeForPhotos = (photos: Photo[]) => {
|
||||||
|
|||||||
@ -239,6 +239,20 @@ 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`
|
||||||
|
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`
|
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
|
||||||
@ -337,7 +351,12 @@ export const getPhoto = async (id: string): Promise<Photo | undefined> => {
|
|||||||
.then(photos => photos.length > 0 ? photos[0] : 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 = () =>
|
export const getPhotosCountIncludingHidden = () =>
|
||||||
safelyQueryPhotos(sqlGetPhotosCountIncludingHidden);
|
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) =>
|
export const pathForPhotoEdit = (photo: PhotoOrPhotoId) =>
|
||||||
`${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/edit`;
|
`${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/edit`;
|
||||||
|
|
||||||
export const pathForTag = (tag: string) =>
|
export const pathForTag = (tag: string, next?: number) =>
|
||||||
`${PREFIX_TAG}/${tag}`;
|
pathWithNext(
|
||||||
|
`${PREFIX_TAG}/${tag}`,
|
||||||
|
next,
|
||||||
|
);
|
||||||
|
|
||||||
export const pathForTagShare = (tag: string) =>
|
export const pathForTagShare = (tag: string) =>
|
||||||
`${pathForTag(tag)}/${SHARE}`;
|
`${pathForTag(tag)}/${SHARE}`;
|
||||||
|
|
||||||
export const pathForCamera = ({ make, model }: Camera) =>
|
export const pathForCamera = ({ make, model }: Camera, next?: number) =>
|
||||||
`${PREFIX_CAMERA}/${createCameraKey(make, model)}`;
|
pathWithNext(
|
||||||
|
`${PREFIX_CAMERA}/${createCameraKey(make, model)}`,
|
||||||
|
next,
|
||||||
|
);
|
||||||
|
|
||||||
export const pathForCameraShare = (camera: Camera) =>
|
export const pathForCameraShare = (camera: Camera) =>
|
||||||
`${pathForCamera(camera)}/${SHARE}`;
|
`${pathForCamera(camera)}/${SHARE}`;
|
||||||
|
|||||||
@ -8,19 +8,22 @@ export default function TagHeader({
|
|||||||
tag,
|
tag,
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
|
count,
|
||||||
}: {
|
}: {
|
||||||
tag: string
|
tag: string
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
selectedPhoto?: Photo
|
selectedPhoto?: Photo
|
||||||
|
count?: number
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
entity={<PhotoTag tag={tag} />}
|
entity={<PhotoTag tag={tag} />}
|
||||||
entityVerb="Tagged"
|
entityVerb="Tagged"
|
||||||
entityDescription={descriptionForTaggedPhotos(photos)}
|
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
sharePath={pathForTagShare(tag)}
|
sharePath={pathForTagShare(tag)}
|
||||||
|
count={count}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export default function TagOGTile({
|
|||||||
onLoad,
|
onLoad,
|
||||||
onFail,
|
onFail,
|
||||||
retryTime,
|
retryTime,
|
||||||
|
count,
|
||||||
}: {
|
}: {
|
||||||
tag: string
|
tag: string
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
@ -21,11 +22,12 @@ export default function TagOGTile({
|
|||||||
onFail?: () => void
|
onFail?: () => void
|
||||||
riseOnHover?: boolean
|
riseOnHover?: boolean
|
||||||
retryTime?: number
|
retryTime?: number
|
||||||
|
count?: number
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<OGTile {...{
|
<OGTile {...{
|
||||||
title: titleForTag(tag, photos),
|
title: titleForTag(tag, photos),
|
||||||
description: descriptionForTaggedPhotos(photos, true),
|
description: descriptionForTaggedPhotos(photos, true, count),
|
||||||
path: pathForTag(tag),
|
path: pathForTag(tag),
|
||||||
pathImageAbsolute: absolutePathForTagImage(tag),
|
pathImageAbsolute: absolutePathForTagImage(tag),
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import TagOGTile from './TagOGTile';
|
|||||||
export default function TagShareModal({
|
export default function TagShareModal({
|
||||||
tag,
|
tag,
|
||||||
photos,
|
photos,
|
||||||
|
count,
|
||||||
}: {
|
}: {
|
||||||
tag: string
|
tag: string
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
|
count?: number
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
@ -16,7 +18,7 @@ export default function TagShareModal({
|
|||||||
pathShare={absolutePathForTag(tag)}
|
pathShare={absolutePathForTag(tag)}
|
||||||
pathClose={pathForTag(tag)}
|
pathClose={pathForTag(tag)}
|
||||||
>
|
>
|
||||||
<TagOGTile tag={tag} photos={photos} />
|
<TagOGTile {...{ tag, photos, count }} />
|
||||||
</ShareModal>
|
</ShareModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,20 +2,29 @@ import { Photo, 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';
|
||||||
|
|
||||||
export const titleForTag = (tag: string, photos:Photo[]) => [
|
export const titleForTag = (
|
||||||
|
tag: string,
|
||||||
|
photos:Photo[],
|
||||||
|
explicitCount?: number,
|
||||||
|
) => [
|
||||||
capitalizeWords(tag.replaceAll('-', ' ')),
|
capitalizeWords(tag.replaceAll('-', ' ')),
|
||||||
photoQuantityText(photos),
|
photoQuantityText(explicitCount ?? photos.length),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const descriptionForTaggedPhotos = (
|
export const descriptionForTaggedPhotos = (
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
dateBased?: boolean,
|
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),
|
url: absolutePathForTag(tag),
|
||||||
title: titleForTag(tag, photos),
|
title: titleForTag(tag, photos, explicitCount),
|
||||||
description: descriptionForTaggedPhotos(photos, true),
|
description: descriptionForTaggedPhotos(photos, true),
|
||||||
images: absolutePathForTagImage(tag),
|
images: absolutePathForTagImage(tag),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user