Create tag-level photo view
This commit is contained in:
parent
aa43ee3012
commit
858a314018
@ -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}
|
||||
<div className="md:space-y-8">
|
||||
<AnimateItems
|
||||
animateFromAppState
|
||||
items={[<PhotoLarge
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
priority
|
||||
prefetchShare
|
||||
/>]}
|
||||
/>
|
||||
<SiteGrid
|
||||
sideFirstOnMobile
|
||||
contentMain={<PhotoGrid
|
||||
photos={photosAfter.slice(1)}
|
||||
animateOnFirstLoadOnly
|
||||
/>}
|
||||
contentSide={<div className={cc(
|
||||
'grid grid-cols-2',
|
||||
'md:flex md:gap-4',
|
||||
'user-select-none',
|
||||
)}>
|
||||
<PhotoLinks photo={photo} photos={photos} />
|
||||
</div>}
|
||||
/>
|
||||
</div>
|
||||
<PhotoDetailPage
|
||||
photo={photo}
|
||||
photos={photos}
|
||||
photosGrid={photosAfter.slice(1)}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
|
||||
@ -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('/'); }
|
||||
|
||||
69
src/app/(static)/t/[tag]/[photoId]/layout.tsx
Normal file
69
src/app/(static)/t/[tag]/[photoId]/layout.tsx
Normal file
@ -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<Metadata> {
|
||||
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}
|
||||
<PhotoDetailPage
|
||||
photo={photo}
|
||||
photos={photos}
|
||||
tag={tag}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
3
src/app/(static)/t/[tag]/[photoId]/page.tsx
Normal file
3
src/app/(static)/t/[tag]/[photoId]/page.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Page() {
|
||||
return null;
|
||||
}
|
||||
17
src/app/(static)/t/[tag]/[photoId]/share/page.tsx
Normal file
17
src/app/(static)/t/[tag]/[photoId]/share/page.tsx
Normal file
@ -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 <PhotoModal photo={photo} tag={tag} />;
|
||||
}
|
||||
@ -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 (
|
||||
<SiteGrid
|
||||
contentMain={<div className="space-y-8 mt-4">
|
||||
<div className={cc(
|
||||
'flex flex-col gap-y-0.5',
|
||||
'xs:grid grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
|
||||
)}>
|
||||
<PhotoTag tag={tag} />
|
||||
<span className={cc(
|
||||
'uppercase text-gray-400 dark:text-gray-500',
|
||||
'sm:col-span-2 md:col-span-1 lg:col-span-2',
|
||||
)}>
|
||||
{descriptionForTaggedPhotos(photos)}
|
||||
</span>
|
||||
<span className={cc(
|
||||
'hidden sm:inline-block',
|
||||
'text-right uppercase',
|
||||
'text-gray-400 dark:text-gray-500',
|
||||
)}>
|
||||
{start === end
|
||||
? start
|
||||
: <>{start}<br />– {end}</>}
|
||||
</span>
|
||||
</div>
|
||||
<PhotoGrid photos={photos} />
|
||||
<TagHeader tag={tag} photos={photos} />
|
||||
<PhotoGrid photos={photos} tag={tag} />
|
||||
</div>}
|
||||
/>
|
||||
);
|
||||
|
||||
64
src/photo/PhotoDetailPage.tsx
Normal file
64
src/photo/PhotoDetailPage.tsx
Normal file
@ -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 (
|
||||
<div>
|
||||
{tag &&
|
||||
<SiteGrid
|
||||
className="mt-4 mb-8"
|
||||
contentMain={
|
||||
<TagHeader
|
||||
key={tag}
|
||||
tag={tag}
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
/>}
|
||||
/>}
|
||||
<AnimateItems
|
||||
className="md:mb-8"
|
||||
animateFromAppState
|
||||
items={[
|
||||
<PhotoLarge
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
tag={tag}
|
||||
priority
|
||||
prefetchShare
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<SiteGrid
|
||||
sideFirstOnMobile
|
||||
contentMain={<PhotoGrid
|
||||
photos={photosGrid ?? photos}
|
||||
selectedPhoto={photo}
|
||||
animateOnFirstLoadOnly
|
||||
/>}
|
||||
contentSide={<div className={cc(
|
||||
'grid grid-cols-2',
|
||||
'md:flex md:gap-4',
|
||||
'user-select-none',
|
||||
)}>
|
||||
<PhotoLinks photo={photo} photos={photos} tag={tag} />
|
||||
</div>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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({
|
||||
<PhotoSmall
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
tag={tag}
|
||||
selected={photo.id === selectedPhoto?.id}
|
||||
/>)}
|
||||
/>
|
||||
|
||||
@ -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({
|
||||
<ImageLarge
|
||||
className="w-full"
|
||||
alt={titleForPhoto(photo)}
|
||||
href={pathForPhoto(photo)}
|
||||
href={pathForPhoto(photo, tag)}
|
||||
src={photo.url}
|
||||
aspectRatio={photo.aspectRatio}
|
||||
blurData={photo.blurData}
|
||||
@ -48,7 +50,7 @@ export default function PhotoLarge({
|
||||
)}>
|
||||
{renderMiniGrid(<>
|
||||
<Link
|
||||
href={pathForPhoto(photo)}
|
||||
href={pathForPhoto(photo, tag)}
|
||||
className="font-bold uppercase"
|
||||
>
|
||||
{titleForPhoto(photo)}
|
||||
@ -93,6 +95,7 @@ export default function PhotoLarge({
|
||||
<div className="-translate-x-0.5">
|
||||
<SharePhotoButton
|
||||
photo={photo}
|
||||
tag={tag}
|
||||
prefetch={prefetchShare}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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
|
||||
? <Link
|
||||
href={pathForPhoto(photo)}
|
||||
href={pathForPhoto(photo, tag)}
|
||||
prefetch={prefetch}
|
||||
onClick={() => {
|
||||
if (nextPhotoAnimation) {
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<PhotoLink
|
||||
photo={previousPhoto}
|
||||
nextPhotoAnimation={ANIMATION_RIGHT}
|
||||
tag={tag}
|
||||
prefetch
|
||||
>
|
||||
PREV
|
||||
@ -65,6 +66,7 @@ export default function PhotoLinks({
|
||||
<PhotoLink
|
||||
photo={nextPhoto}
|
||||
nextPhotoAnimation={ANIMATION_LEFT}
|
||||
tag={tag}
|
||||
prefetch
|
||||
>
|
||||
NEXT
|
||||
|
||||
@ -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 (
|
||||
<Modal onClosePath={pathForPhoto(photo)}>
|
||||
<Modal onClosePath={pathForPhoto(photo, tag)}>
|
||||
<div className="space-y-3 md:space-y-4 w-full">
|
||||
<div className={cc(
|
||||
'flex items-center gap-x-3',
|
||||
|
||||
@ -6,14 +6,16 @@ import { pathForPhoto } from '@/site/paths';
|
||||
|
||||
export default function PhotoSmall({
|
||||
photo,
|
||||
tag,
|
||||
selected,
|
||||
}: {
|
||||
photo: Photo
|
||||
tag?: string
|
||||
selected?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={pathForPhoto(photo)}
|
||||
href={pathForPhoto(photo, tag)}
|
||||
className={cc(
|
||||
'active:brightness-75',
|
||||
selected && 'brightness-50',
|
||||
|
||||
@ -5,17 +5,19 @@ import { cc } from '@/utility/css';
|
||||
import { pathForPhoto } from '@/site/paths';
|
||||
|
||||
export default function PhotoTiny({
|
||||
className,
|
||||
photo,
|
||||
tag,
|
||||
selected,
|
||||
className,
|
||||
}: {
|
||||
className?: string
|
||||
photo: Photo
|
||||
tag?: string
|
||||
selected?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={pathForPhoto(photo)}
|
||||
href={pathForPhoto(photo, tag)}
|
||||
className={cc(
|
||||
className,
|
||||
'active:brightness-75',
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
import { Photo } from '.';
|
||||
import { pathForPhoto } from '@/site/paths';
|
||||
import { pathForPhotoShare } from '@/site/paths';
|
||||
import { TbPhotoShare } from 'react-icons/tb';
|
||||
import IconPathButton from '@/components/IconPathButton';
|
||||
|
||||
export default function SharePhotoButton({
|
||||
photo,
|
||||
tag,
|
||||
prefetch,
|
||||
}: {
|
||||
photo: Photo
|
||||
tag?: string
|
||||
prefetch?: boolean
|
||||
}) {
|
||||
return (
|
||||
<IconPathButton
|
||||
icon={<TbPhotoShare size={17} />}
|
||||
path={pathForPhoto(photo, true)}
|
||||
path={pathForPhotoShare(photo, tag)}
|
||||
prefetch={prefetch}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)}`;
|
||||
|
||||
46
src/tag/TagHeader.tsx
Normal file
46
src/tag/TagHeader.tsx
Normal file
@ -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 (
|
||||
<div className={cc(
|
||||
'flex flex-col gap-y-0.5',
|
||||
'xs:grid grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
|
||||
)}>
|
||||
<PhotoTag tag={tag} />
|
||||
<span className={cc(
|
||||
'uppercase text-gray-400 dark:text-gray-500',
|
||||
'sm:col-span-2 md:col-span-1 lg:col-span-2',
|
||||
)}>
|
||||
{selectedPhotoIndex !== undefined
|
||||
? `Tagged photo ${selectedPhotoIndex + 1} of ${photos.length}`
|
||||
: descriptionForTaggedPhotos(photos)}
|
||||
</span>
|
||||
<span className={cc(
|
||||
'hidden sm:inline-block',
|
||||
'text-right uppercase',
|
||||
'text-gray-400 dark:text-gray-500',
|
||||
)}>
|
||||
{start === end
|
||||
? start
|
||||
: <>{start}<br />– {end}</>}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user