diff --git a/src/app/(static)/p/[photoId]/layout.tsx b/src/app/(static)/p/[photoId]/layout.tsx
index dea77733..daa2200a 100644
--- a/src/app/(static)/p/[photoId]/layout.tsx
+++ b/src/app/(static)/p/[photoId]/layout.tsx
@@ -1,24 +1,17 @@
-import AnimateItems from '@/components/AnimateItems';
-import PhotoLinks from '@/photo/PhotoLinks';
-import SiteGrid from '@/components/SiteGrid';
import {
+ GRID_THUMBNAILS_TO_SHOW_MAX,
ogImageDescriptionForPhoto,
titleForPhoto,
} from '@/photo';
-import PhotoGrid from '@/photo/PhotoGrid';
-import PhotoLarge from '@/photo/PhotoLarge';
-import { cc } from '@/utility/css';
import { Metadata } from 'next';
-import { BASE_URL } from '@/site/config';
import {
getPhoto,
getPhotosTakenAfterPhotoInclusive,
getPhotosTakenBeforePhoto,
} from '@/services/postgres';
import { redirect } from 'next/navigation';
-import { absolutePathForPhotoImage } from '@/site/paths';
-
-const THUMBNAILS_TO_SHOW_MAX = 12;
+import { absolutePathForPhoto, absolutePathForPhotoImage } from '@/site/paths';
+import PhotoDetailPage from '@/photo/PhotoDetailPage';
export const runtime = 'edge';
@@ -34,6 +27,7 @@ export async function generateMetadata({
const title = titleForPhoto(photo);
const description = ogImageDescriptionForPhoto(photo);
const images = absolutePathForPhotoImage(photo);
+ const url = absolutePathForPhoto(photo);
return {
title,
@@ -42,7 +36,7 @@ export async function generateMetadata({
title,
images,
description,
- url: `${BASE_URL}/p/${photo.idShort}`,
+ url,
},
twitter: {
title,
@@ -67,36 +61,16 @@ export default async function PhotoPage({
const photosBefore = await getPhotosTakenBeforePhoto(photo, 1);
const photosAfter = await getPhotosTakenAfterPhotoInclusive(
photo,
- THUMBNAILS_TO_SHOW_MAX + 1,
+ GRID_THUMBNAILS_TO_SHOW_MAX + 1,
);
const photos = photosBefore.concat(photosAfter);
return <>
{children}
-
-
]}
- />
-
}
- contentSide={
}
- />
-
+
>;
}
diff --git a/src/app/(static)/p/[photoId]/share/page.tsx b/src/app/(static)/p/[photoId]/share/page.tsx
index 023e320b..47a309bb 100644
--- a/src/app/(static)/p/[photoId]/share/page.tsx
+++ b/src/app/(static)/p/[photoId]/share/page.tsx
@@ -4,11 +4,11 @@ import { redirect } from 'next/navigation';
export const runtime = 'edge';
-interface Props {
+export default async function Share({
+ params: { photoId },
+}: {
params: { photoId: string }
-}
-
-export default async function Share({ params: { photoId }}: Props) {
+}) {
const photo = await getPhoto(photoId);
if (!photo) { return redirect('/'); }
diff --git a/src/app/(static)/t/[tag]/[photoId]/layout.tsx b/src/app/(static)/t/[tag]/[photoId]/layout.tsx
new file mode 100644
index 00000000..8a7a6487
--- /dev/null
+++ b/src/app/(static)/t/[tag]/[photoId]/layout.tsx
@@ -0,0 +1,69 @@
+import {
+ ogImageDescriptionForPhoto,
+ titleForPhoto,
+} from '@/photo';
+import { Metadata } from 'next';
+import {
+ getPhoto,
+ getPhotos,
+} from '@/services/postgres';
+import { redirect } from 'next/navigation';
+import { absolutePathForPhoto, absolutePathForPhotoImage } from '@/site/paths';
+import PhotoDetailPage from '@/photo/PhotoDetailPage';
+
+export const runtime = 'edge';
+
+export async function generateMetadata({
+ params: { photoId, tag },
+}: {
+ params: { photoId: string, tag: string }
+}): Promise {
+ const photo = await getPhoto(photoId);
+
+ if (!photo) { return {}; }
+
+ const title = titleForPhoto(photo);
+ const description = ogImageDescriptionForPhoto(photo);
+ const images = absolutePathForPhotoImage(photo);
+ const url = absolutePathForPhoto(photo, tag);
+
+ return {
+ title,
+ description,
+ openGraph: {
+ title,
+ images,
+ description,
+ url,
+ },
+ twitter: {
+ title,
+ description,
+ images,
+ card: 'summary_large_image',
+ },
+ };
+}
+
+export default async function PhotoTagPage({
+ params: { photoId, tag },
+ children,
+}: {
+ params: { photoId: string, tag: string }
+ children: React.ReactNode
+}) {
+ const photo = await getPhoto(photoId);
+
+ if (!photo) { redirect('/'); }
+
+ const photos = await getPhotos(undefined, undefined, undefined, tag);
+
+ return <>
+ {children}
+
+ >;
+}
diff --git a/src/app/(static)/t/[tag]/[photoId]/page.tsx b/src/app/(static)/t/[tag]/[photoId]/page.tsx
new file mode 100644
index 00000000..67e08591
--- /dev/null
+++ b/src/app/(static)/t/[tag]/[photoId]/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return null;
+}
diff --git a/src/app/(static)/t/[tag]/[photoId]/share/page.tsx b/src/app/(static)/t/[tag]/[photoId]/share/page.tsx
new file mode 100644
index 00000000..682d5a0a
--- /dev/null
+++ b/src/app/(static)/t/[tag]/[photoId]/share/page.tsx
@@ -0,0 +1,17 @@
+import PhotoModal from '@/photo/PhotoModal';
+import { getPhoto } from '@/services/postgres';
+import { redirect } from 'next/navigation';
+
+export const runtime = 'edge';
+
+export default async function Share({
+ params: { photoId, tag },
+}: {
+ params: { photoId: string, tag: string }
+}) {
+ const photo = await getPhoto(photoId);
+
+ if (!photo) { return redirect('/'); }
+
+ return ;
+}
diff --git a/src/app/(static)/t/[tag]/page.tsx b/src/app/(static)/t/[tag]/page.tsx
index 466986f3..99ae42a4 100644
--- a/src/app/(static)/t/[tag]/page.tsx
+++ b/src/app/(static)/t/[tag]/page.tsx
@@ -1,11 +1,9 @@
import SiteGrid from '@/components/SiteGrid';
-import { dateRangeForPhotos } from '@/photo';
import PhotoGrid from '@/photo/PhotoGrid';
import { getPhotos } from '@/services/postgres';
import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths';
import { descriptionForTaggedPhotos, titleForTag } from '@/tag';
-import PhotoTag from '@/tag/PhotoTag';
-import { cc } from '@/utility/css';
+import TagHeader from '@/tag/TagHeader';
import { Metadata } from 'next';
interface TagProps {
@@ -42,33 +40,11 @@ export async function generateMetadata({
export default async function TagPage({ params: { tag } }: TagProps) {
const photos = await getPhotos(undefined, undefined, undefined, tag);
- const { start, end } = dateRangeForPhotos(photos);
-
return (
-
-
-
- {descriptionForTaggedPhotos(photos)}
-
-
- {start === end
- ? start
- : <>{start}
– {end}>}
-
-
-
+
+
}
/>
);
diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx
new file mode 100644
index 00000000..0ec8356b
--- /dev/null
+++ b/src/photo/PhotoDetailPage.tsx
@@ -0,0 +1,64 @@
+import AnimateItems from '@/components/AnimateItems';
+import { Photo } from '.';
+import PhotoLarge from './PhotoLarge';
+import SiteGrid from '@/components/SiteGrid';
+import PhotoGrid from './PhotoGrid';
+import { cc } from '@/utility/css';
+import PhotoLinks from './PhotoLinks';
+import TagHeader from '@/tag/TagHeader';
+
+export default function PhotoDetailPage({
+ photo,
+ photos,
+ photosGrid,
+ tag,
+}: {
+ photo: Photo
+ photos: Photo[]
+ photosGrid?: Photo[]
+ tag?: string
+}) {
+ return (
+
+ {tag &&
+
}
+ />}
+
,
+ ]}
+ />
+
}
+ contentSide={
}
+ />
+
+ );
+}
diff --git a/src/photo/PhotoGrid.tsx b/src/photo/PhotoGrid.tsx
index 39acdcbd..c627b1e3 100644
--- a/src/photo/PhotoGrid.tsx
+++ b/src/photo/PhotoGrid.tsx
@@ -10,6 +10,7 @@ const PHOTOS_MAX = 35;
export default function PhotoGrid({
photos,
selectedPhoto,
+ tag,
offset = 0,
fast,
animateOnFirstLoadOnly,
@@ -18,6 +19,7 @@ export default function PhotoGrid({
}: {
photos: Photo[]
selectedPhoto?: Photo
+ tag?: string
offset?: number
fast?: boolean
animate?: boolean
@@ -42,6 +44,7 @@ export default function PhotoGrid({
)}
/>
diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx
index a63c9380..4397c6d2 100644
--- a/src/photo/PhotoLarge.tsx
+++ b/src/photo/PhotoLarge.tsx
@@ -9,10 +9,12 @@ import PhotoTags from '@/tag/PhotoTags';
export default function PhotoLarge({
photo,
+ tag,
priority,
prefetchShare,
}: {
photo: Photo
+ tag?: string
priority?: boolean
prefetchShare?: boolean
}) {
@@ -32,7 +34,7 @@ export default function PhotoLarge({
{renderMiniGrid(<>
{titleForPhoto(photo)}
@@ -93,6 +95,7 @@ export default function PhotoLarge({
diff --git a/src/photo/PhotoLink.tsx b/src/photo/PhotoLink.tsx
index e8c083d3..b881c8ac 100644
--- a/src/photo/PhotoLink.tsx
+++ b/src/photo/PhotoLink.tsx
@@ -9,11 +9,13 @@ import { pathForPhoto } from '@/site/paths';
export default function PhotoLink({
photo,
+ tag,
prefetch,
nextPhotoAnimation,
children,
}: {
photo?: Photo
+ tag?: string
prefetch?: boolean
nextPhotoAnimation?: AnimationConfig
children: ReactNode
@@ -23,7 +25,7 @@ export default function PhotoLink({
return (
photo
? {
if (nextPhotoAnimation) {
diff --git a/src/photo/PhotoLinks.tsx b/src/photo/PhotoLinks.tsx
index 4a9565f7..b8104adb 100644
--- a/src/photo/PhotoLinks.tsx
+++ b/src/photo/PhotoLinks.tsx
@@ -3,8 +3,8 @@
import { useEffect } from 'react';
import { Photo, getNextPhoto, getPreviousPhoto } from '@/photo';
import PhotoLink from './PhotoLink';
-import { usePathname, useRouter } from 'next/navigation';
-import { isPathPhotoShare, pathForPhoto } from '@/site/paths';
+import { useRouter } from 'next/navigation';
+import { pathForPhoto } from '@/site/paths';
import { useAppState } from '@/state';
import { AnimationConfig } from '@/components/AnimateItems';
@@ -14,16 +14,16 @@ const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
export default function PhotoLinks({
photo,
photos,
+ tag,
}: {
photo: Photo
photos: Photo[]
+ tag?: string
}) {
const router = useRouter();
- const pathname = usePathname();
const { setNextPhotoAnimation } = useAppState();
- const isRouteShare = isPathPhotoShare(pathname);
const previousPhoto = getPreviousPhoto(photo, photos);
const nextPhoto = getNextPhoto(photo, photos);
@@ -34,14 +34,14 @@ export default function PhotoLinks({
case 'J':
if (previousPhoto) {
setNextPhotoAnimation?.(ANIMATION_RIGHT);
- router.push(pathForPhoto(previousPhoto, isRouteShare));
+ router.push(pathForPhoto(previousPhoto, tag));
}
break;
case 'ARROWRIGHT':
case 'L':
if (nextPhoto) {
setNextPhotoAnimation?.(ANIMATION_LEFT);
- router.push(pathForPhoto(nextPhoto, isRouteShare));
+ router.push(pathForPhoto(nextPhoto, tag));
}
break;
case 'ESCAPE':
@@ -51,13 +51,14 @@ export default function PhotoLinks({
};
window.addEventListener('keyup', onKeyUp);
return () => window.removeEventListener('keyup', onKeyUp);
- }, [router, setNextPhotoAnimation, previousPhoto, nextPhoto, isRouteShare]);
+ }, [router, setNextPhotoAnimation, previousPhoto, nextPhoto, tag]);
return (
<>
PREV
@@ -65,6 +66,7 @@ export default function PhotoLinks({
NEXT
diff --git a/src/photo/PhotoModal.tsx b/src/photo/PhotoModal.tsx
index 7d327bbd..466b4f8a 100644
--- a/src/photo/PhotoModal.tsx
+++ b/src/photo/PhotoModal.tsx
@@ -10,11 +10,17 @@ import { Photo } from '.';
import { toast } from 'sonner';
import { FiCheckSquare } from 'react-icons/fi';
-export default function PhotoModal({ photo }: { photo: Photo }) {
- const shareUrl = absolutePathForPhoto(photo);
+export default function PhotoModal({
+ photo,
+ tag,
+}: {
+ photo: Photo
+ tag?: string
+}) {
+ const shareUrl = absolutePathForPhoto(photo, tag);
return (
-
+
}
- path={pathForPhoto(photo, true)}
+ path={pathForPhotoShare(photo, tag)}
prefetch={prefetch}
/>
);
diff --git a/src/photo/index.ts b/src/photo/index.ts
index 362afa11..2798ba1e 100644
--- a/src/photo/index.ts
+++ b/src/photo/index.ts
@@ -16,6 +16,8 @@ import short from 'short-uuid';
const translator = short();
+export const GRID_THUMBNAILS_TO_SHOW_MAX = 12;
+
// Core EXIF data
export interface PhotoExif {
aspectRatio: number
diff --git a/src/site/paths.ts b/src/site/paths.ts
index f50c4dee..a264aa65 100644
--- a/src/site/paths.ts
+++ b/src/site/paths.ts
@@ -11,18 +11,21 @@ export const PATH_ADMIN_UPLOAD_BLOB_HANDLER = `${PATH_ADMIN_UPLOAD}/blob`;
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
-export const pathForPhoto = (photo: Photo, share?: boolean) =>
- share
- ? `${PREFIX_PHOTO}/${photo.idShort}/share`
+export const pathForPhoto = (photo: Photo, tag?: string) =>
+ tag
+ ? `${pathForTag(tag)}/${photo.idShort}`
: `${PREFIX_PHOTO}/${photo.idShort}`;
+export const pathForPhotoShare = (photo: Photo, tag?: string) =>
+ `${pathForPhoto(photo, tag)}/share`;
+
export const pathForPhotoEdit = (photo: Photo) =>
`${PATH_ADMIN_PHOTOS}/${photo.idShort}/edit`;
export const pathForTag = (tag: string) => `${PREFIX_TAG}/${tag}`;
-export const absolutePathForPhoto = (photo: Photo) =>
- `${BASE_URL}${pathForPhoto(photo)}`;
+export const absolutePathForPhoto = (photo: Photo, tag?: string) =>
+ `${BASE_URL}${pathForPhoto(photo, tag)}`;
export const absolutePathForTag = (tag: string) =>
`${BASE_URL}${pathForTag(tag)}`;
diff --git a/src/tag/TagHeader.tsx b/src/tag/TagHeader.tsx
new file mode 100644
index 00000000..af34b14f
--- /dev/null
+++ b/src/tag/TagHeader.tsx
@@ -0,0 +1,46 @@
+import { Photo, dateRangeForPhotos } from '@/photo';
+import { cc } from '@/utility/css';
+import PhotoTag from './PhotoTag';
+import { descriptionForTaggedPhotos } from '.';
+
+export default function TagHeader({
+ tag,
+ photos,
+ selectedPhoto,
+}: {
+ tag: string
+ photos: Photo[]
+ selectedPhoto?: Photo
+}) {
+ const { start, end } = dateRangeForPhotos(photos);
+
+ const selectedPhotoIndex = selectedPhoto
+ ? photos.findIndex(photo => photo.id === selectedPhoto.id)
+ : undefined;
+
+ return (
+
+
+
+ {selectedPhotoIndex !== undefined
+ ? `Tagged photo ${selectedPhotoIndex + 1} of ${photos.length}`
+ : descriptionForTaggedPhotos(photos)}
+
+
+ {start === end
+ ? start
+ : <>{start}
– {end}>}
+
+
+ );
+}