From 96f8c18893b7b4f0a04590d778f727338d3812f6 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Thu, 21 Sep 2023 20:30:28 -0500 Subject: [PATCH] Add tag share files --- src/app/(static)/p/[photoId]/layout.tsx | 4 +- src/app/(static)/p/[photoId]/share/page.tsx | 4 +- src/app/(static)/t/[tag]/[photoId]/layout.tsx | 4 +- .../(static)/t/[tag]/[photoId]/share/page.tsx | 4 +- src/app/(static)/t/[tag]/page.tsx | 3 +- src/app/(static)/t/[tag]/share/page.tsx | 25 ++++ src/components/IconPathButton.tsx | 1 + src/components/OGTile.tsx | 130 ++++++++++++++++++ src/components/ShareButton.tsx | 25 ++++ .../ShareModal.tsx} | 30 ++-- src/photo/PhotoGrid.tsx | 2 + src/photo/PhotoLarge.tsx | 9 +- src/photo/PhotoOGTile.tsx | 118 ++-------------- src/photo/PhotoShareModal.tsx | 22 +++ src/photo/index.ts | 2 +- src/site/paths.ts | 7 +- src/tag/TagHeader.tsx | 5 + src/tag/TagOGTile.tsx | 38 +++++ src/tag/TagShareModal.tsx | 22 +++ 19 files changed, 319 insertions(+), 136 deletions(-) create mode 100644 src/app/(static)/t/[tag]/share/page.tsx create mode 100644 src/components/OGTile.tsx create mode 100644 src/components/ShareButton.tsx rename src/{photo/PhotoModal.tsx => components/ShareModal.tsx} (75%) create mode 100644 src/photo/PhotoShareModal.tsx create mode 100644 src/tag/TagOGTile.tsx create mode 100644 src/tag/TagShareModal.tsx diff --git a/src/app/(static)/p/[photoId]/layout.tsx b/src/app/(static)/p/[photoId]/layout.tsx index fb8c6320..534abd02 100644 --- a/src/app/(static)/p/[photoId]/layout.tsx +++ b/src/app/(static)/p/[photoId]/layout.tsx @@ -1,6 +1,6 @@ import { GRID_THUMBNAILS_TO_SHOW_MAX, - ogImageDescriptionForPhoto, + descriptionForPhoto, titleForPhoto, } from '@/photo'; import { Metadata } from 'next'; @@ -27,7 +27,7 @@ export async function generateMetadata({ if (!photo) { return {}; } const title = titleForPhoto(photo); - const description = ogImageDescriptionForPhoto(photo); + const description = descriptionForPhoto(photo); const images = absolutePathForPhotoImage(photo); const url = absolutePathForPhoto(photo); diff --git a/src/app/(static)/p/[photoId]/share/page.tsx b/src/app/(static)/p/[photoId]/share/page.tsx index 811af5a2..5fc0c871 100644 --- a/src/app/(static)/p/[photoId]/share/page.tsx +++ b/src/app/(static)/p/[photoId]/share/page.tsx @@ -1,5 +1,5 @@ import { getPhotoCached } from '@/cache'; -import PhotoModal from '@/photo/PhotoModal'; +import PhotoShareModal from '@/photo/PhotoShareModal'; import { redirect } from 'next/navigation'; export const runtime = 'edge'; @@ -13,5 +13,5 @@ export default async function Share({ if (!photo) { return redirect('/'); } - return ; + return ; } diff --git a/src/app/(static)/t/[tag]/[photoId]/layout.tsx b/src/app/(static)/t/[tag]/[photoId]/layout.tsx index 06bad5eb..bd7a062a 100644 --- a/src/app/(static)/t/[tag]/[photoId]/layout.tsx +++ b/src/app/(static)/t/[tag]/[photoId]/layout.tsx @@ -1,5 +1,5 @@ import { - ogImageDescriptionForPhoto, + descriptionForPhoto, titleForPhoto, } from '@/photo'; import { Metadata } from 'next'; @@ -33,7 +33,7 @@ export async function generateMetadata({ if (!photo) { return {}; } const title = titleForPhoto(photo); - const description = ogImageDescriptionForPhoto(photo); + const description = descriptionForPhoto(photo); const images = absolutePathForPhotoImage(photo); const url = absolutePathForPhoto(photo, tag); diff --git a/src/app/(static)/t/[tag]/[photoId]/share/page.tsx b/src/app/(static)/t/[tag]/[photoId]/share/page.tsx index b6480874..6fb28fef 100644 --- a/src/app/(static)/t/[tag]/[photoId]/share/page.tsx +++ b/src/app/(static)/t/[tag]/[photoId]/share/page.tsx @@ -1,5 +1,5 @@ import { getPhotoCached } from '@/cache'; -import PhotoModal from '@/photo/PhotoModal'; +import PhotoShareModal from '@/photo/PhotoShareModal'; import { redirect } from 'next/navigation'; export const runtime = 'edge'; @@ -13,5 +13,5 @@ export default async function Share({ if (!photo) { return redirect('/'); } - return ; + return ; } diff --git a/src/app/(static)/t/[tag]/page.tsx b/src/app/(static)/t/[tag]/page.tsx index b327c8fc..2e2b7d3f 100644 --- a/src/app/(static)/t/[tag]/page.tsx +++ b/src/app/(static)/t/[tag]/page.tsx @@ -45,11 +45,12 @@ export async function generateMetadata({ }; } -export default async function TagPage({ params: { tag } }: TagProps) { +export default async function TagPage({ params: { tag } }:TagProps) { const photos = await getPhotosCached({ tag }); return ( diff --git a/src/app/(static)/t/[tag]/share/page.tsx b/src/app/(static)/t/[tag]/share/page.tsx new file mode 100644 index 00000000..5cf96c94 --- /dev/null +++ b/src/app/(static)/t/[tag]/share/page.tsx @@ -0,0 +1,25 @@ +import { getPhotosCached } from '@/cache'; +import SiteGrid from '@/components/SiteGrid'; +import PhotoGrid from '@/photo/PhotoGrid'; +import TagHeader from '@/tag/TagHeader'; +import TagShareModal from '@/tag/TagShareModal'; + +export const runtime = 'edge'; + +export default async function Share({ + params: { tag }, +}: { + params: { tag: string } +}) { + const photos = await getPhotosCached({ tag }); + return <> + + + + + } + /> + ; +} diff --git a/src/components/IconPathButton.tsx b/src/components/IconPathButton.tsx index 8cb41c0b..64d6972b 100644 --- a/src/components/IconPathButton.tsx +++ b/src/components/IconPathButton.tsx @@ -48,6 +48,7 @@ export default function IconPathButton({ router.push(path, { scroll: shouldScroll }))} isLoading={shouldShowLoader} className={cc( + 'translate-y-[-0.5px]', 'active:translate-y-[1px]', 'text-gray-500 active:text-gray-600', 'dark:text-gray-400 dark:active:text-gray-300', diff --git a/src/components/OGTile.tsx b/src/components/OGTile.tsx new file mode 100644 index 00000000..868a9b8e --- /dev/null +++ b/src/components/OGTile.tsx @@ -0,0 +1,130 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { cc } from '@/utility/css'; +import Link from 'next/link'; +import { BiError } from 'react-icons/bi'; +import Spinner from '@/components/Spinner'; +import { IMAGE_OG_SIZE } from '../photo/image-response'; + +export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed'; + +export default function OGTile({ + title, + description, + path, + pathImageAbsolute, + loadingState: loadingStateExternal, + riseOnHover, + onLoad, + onFail, + retryTime, +}: { + title: string + description: string + path: string + pathImageAbsolute: string + loadingState?: OGLoadingState + onLoad?: () => void + onFail?: () => void + riseOnHover?: boolean + retryTime?: number +}) { + const [loadingStateInternal, setLoadingStateInternal] = + useState(loadingStateExternal ?? 'unloaded'); + + const loadingState = loadingStateExternal ?? loadingStateInternal; + + useEffect(() => { + if ( + !loadingStateExternal && + loadingStateInternal === 'unloaded' + ) { + setLoadingStateInternal('loading'); + } + }, [loadingStateExternal, loadingStateInternal]); + + const { width, height, ratio } = IMAGE_OG_SIZE; + + return ( + +
+ {loadingState === 'loading' && +
+ +
} + {loadingState === 'failed' && +
+ +
} + {(loadingState === 'loading' || loadingState === 'loaded') && + {title} { + if (onLoad) { + onLoad(); + } else { + setLoadingStateInternal('loaded'); + } + }} + onError={() => { + if (onFail) { + onFail(); + } else { + setLoadingStateInternal('failed'); + } + if (retryTime !== undefined) { + setTimeout(() => { + setLoadingStateInternal('loading'); + }, retryTime); + } + }} + />} +
+
+
+ {title} +
+
+ {description} +
+
+ + ); +}; diff --git a/src/components/ShareButton.tsx b/src/components/ShareButton.tsx new file mode 100644 index 00000000..936ab557 --- /dev/null +++ b/src/components/ShareButton.tsx @@ -0,0 +1,25 @@ +import { TbPhotoShare } from 'react-icons/tb'; +import IconPathButton from '@/components/IconPathButton'; + +export default function ShareButton({ + path, + prefetch, + shouldScroll, + dim, +}: { + path: string + prefetch?: boolean + shouldScroll?: boolean + dim?: boolean +}) { + return ( + , + prefetch, + shouldScroll, + }} /> + ); +} diff --git a/src/photo/PhotoModal.tsx b/src/components/ShareModal.tsx similarity index 75% rename from src/photo/PhotoModal.tsx rename to src/components/ShareModal.tsx index 57443dc7..59f19d7e 100644 --- a/src/photo/PhotoModal.tsx +++ b/src/components/ShareModal.tsx @@ -1,26 +1,26 @@ 'use client'; import Modal from '@/components/Modal'; -import PhotoOGTile from '@/photo/PhotoOGTile'; -import { absolutePathForPhoto, pathForPhoto } from '@/site/paths'; import { TbPhotoShare } from 'react-icons/tb'; import { cc } from '@/utility/css'; import { BiCopy } from 'react-icons/bi'; -import { Photo } from '.'; import { toast } from 'sonner'; import { FiCheckSquare } from 'react-icons/fi'; +import { ReactNode } from 'react'; -export default function PhotoModal({ - photo, - tag, +export default function ShareModal({ + title = 'Share', + pathShare, + pathClose, + children, }: { - photo: Photo - tag?: string + title?: string + pathShare: string + pathClose: string + children: ReactNode }) { - const shareUrl = absolutePathForPhoto(photo, tag); - return ( - +
- Share Photo + {title}
- + {children}
- {shareUrl} + {pathShare}
{ - navigator.clipboard.writeText(shareUrl); + navigator.clipboard.writeText(pathShare); toast( 'Link to photo copied', { icon: }, diff --git a/src/photo/PhotoGrid.tsx b/src/photo/PhotoGrid.tsx index c627b1e3..3ebb0f8f 100644 --- a/src/photo/PhotoGrid.tsx +++ b/src/photo/PhotoGrid.tsx @@ -13,6 +13,7 @@ export default function PhotoGrid({ tag, offset = 0, fast, + animate = true, animateOnFirstLoadOnly, staggerOnFirstLoadOnly = true, showMore, @@ -35,6 +36,7 @@ export default function PhotoGrid({ '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} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index e0202e2c..1510411c 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -3,9 +3,9 @@ import SiteGrid from '@/components/SiteGrid'; import ImageLarge from '@/components/ImageLarge'; import { cc } from '@/utility/css'; import Link from 'next/link'; -import { pathForPhoto } from '@/site/paths'; -import SharePhotoButton from './SharePhotoButton'; +import { pathForPhoto, pathForPhotoShare } from '@/site/paths'; import PhotoTags from '@/tag/PhotoTags'; +import ShareButton from '@/components/ShareButton'; export default function PhotoLarge({ photo, @@ -99,9 +99,8 @@ export default function PhotoLarge({ {photo.takenAtNaiveFormatted}
- diff --git a/src/photo/PhotoOGTile.tsx b/src/photo/PhotoOGTile.tsx index 20c39525..2dd8ce87 100644 --- a/src/photo/PhotoOGTile.tsx +++ b/src/photo/PhotoOGTile.tsx @@ -1,17 +1,10 @@ -'use client'; - -import { useEffect, useState } from 'react'; import { Photo, - ogImageDescriptionForPhoto, + descriptionForPhoto, titleForPhoto, } from '@/photo'; -import { cc } from '@/utility/css'; -import Link from 'next/link'; -import { BiError } from 'react-icons/bi'; import { absolutePathForPhotoImage, pathForPhoto } from '@/site/paths'; -import Spinner from '@/components/Spinner'; -import { IMAGE_OG_SIZE } from './image-response'; +import OGTile from '@/components/OGTile'; export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed'; @@ -30,102 +23,17 @@ export default function PhotoOGTile({ riseOnHover?: boolean retryTime?: number }) { - const [loadingStateInternal, setLoadingStateInternal] = - useState(loadingStateExternal ?? 'unloaded'); - - const loadingState = loadingStateExternal ?? loadingStateInternal; - - useEffect(() => { - if ( - !loadingStateExternal && - loadingStateInternal === 'unloaded' - ) { - setLoadingStateInternal('loading'); - } - }, [loadingStateExternal, loadingStateInternal]); - - const { width, height, ratio } = IMAGE_OG_SIZE; - return ( - -
- {loadingState === 'loading' && -
- -
} - {loadingState === 'failed' && -
- -
} - {(loadingState === 'loading' || loadingState === 'loaded') && - {titleForPhoto(photo)} { - if (onLoad) { - onLoad(); - } else { - setLoadingStateInternal('loaded'); - } - }} - onError={() => { - if (onFail) { - onFail(); - } else { - setLoadingStateInternal('failed'); - } - if (retryTime !== undefined) { - setTimeout(() => { - setLoadingStateInternal('loading'); - }, retryTime); - } - }} - />} -
-
-
- {titleForPhoto(photo)} -
-
- {ogImageDescriptionForPhoto(photo)} -
-
- + ); }; diff --git a/src/photo/PhotoShareModal.tsx b/src/photo/PhotoShareModal.tsx new file mode 100644 index 00000000..a78c5cb7 --- /dev/null +++ b/src/photo/PhotoShareModal.tsx @@ -0,0 +1,22 @@ +import PhotoOGTile from '@/photo/PhotoOGTile'; +import { absolutePathForPhoto, pathForPhoto } from '@/site/paths'; +import { Photo } from '.'; +import ShareModal from '@/components/ShareModal'; + +export default function PhotoShareModal({ + photo, + tag, +}: { + photo: Photo + tag?: string +}) { + return ( + + + + ); +}; diff --git a/src/photo/index.ts b/src/photo/index.ts index 31407a19..31457165 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -115,7 +115,7 @@ export const photoStatsAsString = (photo: Photo) => [ photo.isoFormatted, ].join(' '); -export const ogImageDescriptionForPhoto = (photo: Photo) => +export const descriptionForPhoto = (photo: Photo) => photo.takenAtNaiveFormatted?.toUpperCase(); export const getPreviousPhoto = (photo: Photo, photos: Photo[]) => { diff --git a/src/site/paths.ts b/src/site/paths.ts index 9d5a69ee..b286f04e 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -5,6 +5,8 @@ const PREFIX_PHOTO = '/p'; const PREFIX_TAG = '/t'; const PREFIX_ADMIN = '/admin'; +const SHARE = 'share'; + export const PATH_ADMIN_PHOTOS = `${PREFIX_ADMIN}/photos`; export const PATH_ADMIN_UPLOAD = `${PREFIX_ADMIN}/uploads`; export const PATH_ADMIN_UPLOAD_BLOB_HANDLER = `${PATH_ADMIN_UPLOAD}/blob`; @@ -17,13 +19,16 @@ export const pathForPhoto = (photo: Photo, tag?: string) => : `${PREFIX_PHOTO}/${photo.id}`; export const pathForPhotoShare = (photo: Photo, tag?: string) => - `${pathForPhoto(photo, tag)}/share`; + `${pathForPhoto(photo, tag)}/${SHARE}`; export const pathForPhotoEdit = (photo: Photo) => `${PATH_ADMIN_PHOTOS}/${photo.id}/edit`; export const pathForTag = (tag: string) => `${PREFIX_TAG}/${tag}`; +export const pathForTagShare = (tag: string) => + `${pathForTag(tag)}/${SHARE}`; + export const absolutePathForPhoto = (photo: Photo, tag?: string) => `${BASE_URL}${pathForPhoto(photo, tag)}`; diff --git a/src/tag/TagHeader.tsx b/src/tag/TagHeader.tsx index 4790a1b1..0f25aa49 100644 --- a/src/tag/TagHeader.tsx +++ b/src/tag/TagHeader.tsx @@ -2,6 +2,8 @@ import { Photo, dateRangeForPhotos } from '@/photo'; import { cc } from '@/utility/css'; import PhotoTag from './PhotoTag'; import { descriptionForTaggedPhotos } from '.'; +import ShareButton from '@/components/ShareButton'; +import { pathForTagShare } from '@/site/paths'; export default function TagHeader({ tag, @@ -25,12 +27,15 @@ export default function TagHeader({ )}> {selectedPhotoIndex !== undefined ? `Tagged ${selectedPhotoIndex + 1} of ${photos.length}` : descriptionForTaggedPhotos(photos)} + {selectedPhotoIndex === undefined && + } void + onFail?: () => void + riseOnHover?: boolean + retryTime?: number +}) { + return ( + + ); +}; diff --git a/src/tag/TagShareModal.tsx b/src/tag/TagShareModal.tsx new file mode 100644 index 00000000..55bff1cf --- /dev/null +++ b/src/tag/TagShareModal.tsx @@ -0,0 +1,22 @@ +import { absolutePathForTag, pathForTag } from '@/site/paths'; +import { Photo } from '../photo'; +import ShareModal from '@/components/ShareModal'; +import TagOGTile from './TagOGTile'; + +export default function TagShareModal({ + tag, + photos, +}: { + tag: string + photos: Photo[] +}) { + return ( + + + + ); +};