From 858a31401879d05e66a6f67a2c2a268f1ac35498 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 18 Sep 2023 19:02:36 -0500 Subject: [PATCH] Create tag-level photo view --- src/app/(static)/p/[photoId]/layout.tsx | 48 +++---------- src/app/(static)/p/[photoId]/share/page.tsx | 8 +-- src/app/(static)/t/[tag]/[photoId]/layout.tsx | 69 +++++++++++++++++++ src/app/(static)/t/[tag]/[photoId]/page.tsx | 3 + .../(static)/t/[tag]/[photoId]/share/page.tsx | 17 +++++ src/app/(static)/t/[tag]/page.tsx | 30 +------- src/photo/PhotoDetailPage.tsx | 64 +++++++++++++++++ src/photo/PhotoGrid.tsx | 3 + src/photo/PhotoLarge.tsx | 7 +- src/photo/PhotoLink.tsx | 4 +- src/photo/PhotoLinks.tsx | 16 +++-- src/photo/PhotoModal.tsx | 12 +++- src/photo/PhotoSmall.tsx | 4 +- src/photo/PhotoTiny.tsx | 8 ++- src/photo/SharePhotoButton.tsx | 6 +- src/photo/index.ts | 2 + src/site/paths.ts | 13 ++-- src/tag/TagHeader.tsx | 46 +++++++++++++ 18 files changed, 268 insertions(+), 92 deletions(-) create mode 100644 src/app/(static)/t/[tag]/[photoId]/layout.tsx create mode 100644 src/app/(static)/t/[tag]/[photoId]/page.tsx create mode 100644 src/app/(static)/t/[tag]/[photoId]/share/page.tsx create mode 100644 src/photo/PhotoDetailPage.tsx create mode 100644 src/tag/TagHeader.tsx 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}} +
+
+ ); +}