Add tag share files
This commit is contained in:
parent
a023cbf311
commit
96f8c18893
@ -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);
|
||||
|
||||
|
||||
@ -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 <PhotoModal photo={photo} />;
|
||||
return <PhotoShareModal photo={photo} />;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 <PhotoModal photo={photo} tag={tag} />;
|
||||
return <PhotoShareModal photo={photo} tag={tag} />;
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<SiteGrid
|
||||
key="Tag Grid"
|
||||
contentMain={<div className="space-y-8 mt-4">
|
||||
<TagHeader tag={tag} photos={photos} />
|
||||
<PhotoGrid photos={photos} tag={tag} />
|
||||
|
||||
25
src/app/(static)/t/[tag]/share/page.tsx
Normal file
25
src/app/(static)/t/[tag]/share/page.tsx
Normal file
@ -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 <>
|
||||
<TagShareModal tag={tag} photos={photos} />
|
||||
<SiteGrid
|
||||
key="Tag Grid"
|
||||
contentMain={<div className="space-y-8 mt-4">
|
||||
<TagHeader tag={tag} photos={photos} />
|
||||
<PhotoGrid photos={photos} tag={tag} animate={false} />
|
||||
</div>}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
@ -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',
|
||||
|
||||
130
src/components/OGTile.tsx
Normal file
130
src/components/OGTile.tsx
Normal file
@ -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 (
|
||||
<Link
|
||||
href={path}
|
||||
className={cc(
|
||||
'group',
|
||||
'block w-full rounded-md overflow-hidden',
|
||||
'border shadow-sm',
|
||||
'border-gray-200 dark:border-gray-800',
|
||||
riseOnHover && 'hover:-translate-y-1.5 transition-transform',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="relative"
|
||||
style={{ aspectRatio: ratio }}
|
||||
>
|
||||
{loadingState === 'loading' &&
|
||||
<div className={cc(
|
||||
'absolute top-0 left-0 right-0 bottom-0 z-10',
|
||||
'flex items-center justify-center',
|
||||
)}>
|
||||
<Spinner size={40} />
|
||||
</div>}
|
||||
{loadingState === 'failed' &&
|
||||
<div className={cc(
|
||||
'absolute top-0 left-0 right-0 bottom-0 z-[11]',
|
||||
'flex items-center justify-center',
|
||||
'text-red-400',
|
||||
)}>
|
||||
<BiError size={32} />
|
||||
</div>}
|
||||
{(loadingState === 'loading' || loadingState === 'loaded') &&
|
||||
<img
|
||||
alt={title}
|
||||
className={cc(
|
||||
'absolute top-0 left-0 right-0 bottom-0 z-0',
|
||||
'w-full',
|
||||
loadingState === 'loading' && 'opacity-0',
|
||||
'transition-opacity',
|
||||
)}
|
||||
src={pathImageAbsolute}
|
||||
width={width}
|
||||
height={height}
|
||||
onLoad={() => {
|
||||
if (onLoad) {
|
||||
onLoad();
|
||||
} else {
|
||||
setLoadingStateInternal('loaded');
|
||||
}
|
||||
}}
|
||||
onError={() => {
|
||||
if (onFail) {
|
||||
onFail();
|
||||
} else {
|
||||
setLoadingStateInternal('failed');
|
||||
}
|
||||
if (retryTime !== undefined) {
|
||||
setTimeout(() => {
|
||||
setLoadingStateInternal('loading');
|
||||
}, retryTime);
|
||||
}
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
<div className={cc(
|
||||
'md:text-lg',
|
||||
'flex flex-col gap-1 p-3',
|
||||
'font-sans leading-none',
|
||||
'bg-gray-50 dark:bg-gray-900/50',
|
||||
'group-active:bg-gray-50 group-active:dark:bg-gray-900/50',
|
||||
'group-hover:bg-gray-100 group-hover:dark:bg-gray-900/70',
|
||||
'border-t border-gray-200 dark:border-gray-800',
|
||||
)}>
|
||||
<div className="text-gray-800 dark:text-white font-medium">
|
||||
{title}
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
25
src/components/ShareButton.tsx
Normal file
25
src/components/ShareButton.tsx
Normal file
@ -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 (
|
||||
<IconPathButton {...{
|
||||
path,
|
||||
icon: <TbPhotoShare size={17} className={dim
|
||||
? 'text-gray-400 dark:text-gray-500'
|
||||
: undefined} />,
|
||||
prefetch,
|
||||
shouldScroll,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<Modal onClosePath={pathForPhoto(photo, tag)}>
|
||||
<Modal onClosePath={pathClose}>
|
||||
<div className="space-y-3 md:space-y-4 w-full">
|
||||
<div className={cc(
|
||||
'flex items-center gap-x-3',
|
||||
@ -28,10 +28,10 @@ export default function PhotoModal({
|
||||
)}>
|
||||
<TbPhotoShare size={22} className="hidden xs:block" />
|
||||
<div className="flex-grow">
|
||||
Share Photo
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<PhotoOGTile photo={photo} />
|
||||
{children}
|
||||
<div className={cc(
|
||||
'rounded-md',
|
||||
'w-full overflow-hidden',
|
||||
@ -39,7 +39,7 @@ export default function PhotoModal({
|
||||
'border border-gray-200 dark:border-gray-800',
|
||||
)}>
|
||||
<div className="truncate p-2 w-full">
|
||||
{shareUrl}
|
||||
{pathShare}
|
||||
</div>
|
||||
<div
|
||||
className={cc(
|
||||
@ -50,7 +50,7 @@ export default function PhotoModal({
|
||||
'cursor-pointer',
|
||||
)}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
navigator.clipboard.writeText(pathShare);
|
||||
toast(
|
||||
'Link to photo copied',
|
||||
{ icon: <FiCheckSquare size={16} /> },
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
</div>
|
||||
<div className="-translate-x-0.5">
|
||||
<SharePhotoButton
|
||||
photo={photo}
|
||||
tag={tag}
|
||||
<ShareButton
|
||||
path={pathForPhotoShare(photo, tag)}
|
||||
prefetch={prefetchShare}
|
||||
shouldScroll={shouldScrollOnShare}
|
||||
/>
|
||||
|
||||
@ -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 (
|
||||
<Link
|
||||
key={photo.id}
|
||||
href={pathForPhoto(photo)}
|
||||
className={cc(
|
||||
'group',
|
||||
'block w-full rounded-md overflow-hidden',
|
||||
'border shadow-sm',
|
||||
'border-gray-200 dark:border-gray-800',
|
||||
riseOnHover && 'hover:-translate-y-1.5 transition-transform',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="relative"
|
||||
style={{ aspectRatio: ratio }}
|
||||
>
|
||||
{loadingState === 'loading' &&
|
||||
<div className={cc(
|
||||
'absolute top-0 left-0 right-0 bottom-0 z-10',
|
||||
'flex items-center justify-center',
|
||||
)}>
|
||||
<Spinner size={40} />
|
||||
</div>}
|
||||
{loadingState === 'failed' &&
|
||||
<div className={cc(
|
||||
'absolute top-0 left-0 right-0 bottom-0 z-[11]',
|
||||
'flex items-center justify-center',
|
||||
'text-red-400',
|
||||
)}>
|
||||
<BiError size={32} />
|
||||
</div>}
|
||||
{(loadingState === 'loading' || loadingState === 'loaded') &&
|
||||
<img
|
||||
alt={titleForPhoto(photo)}
|
||||
className={cc(
|
||||
'absolute top-0 left-0 right-0 bottom-0 z-0',
|
||||
'w-full',
|
||||
loadingState === 'loading' && 'opacity-0',
|
||||
'transition-opacity',
|
||||
)}
|
||||
src={absolutePathForPhotoImage(photo)}
|
||||
width={width}
|
||||
height={height}
|
||||
onLoad={() => {
|
||||
if (onLoad) {
|
||||
onLoad();
|
||||
} else {
|
||||
setLoadingStateInternal('loaded');
|
||||
}
|
||||
}}
|
||||
onError={() => {
|
||||
if (onFail) {
|
||||
onFail();
|
||||
} else {
|
||||
setLoadingStateInternal('failed');
|
||||
}
|
||||
if (retryTime !== undefined) {
|
||||
setTimeout(() => {
|
||||
setLoadingStateInternal('loading');
|
||||
}, retryTime);
|
||||
}
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
<div className={cc(
|
||||
'md:text-lg',
|
||||
'flex flex-col gap-1 p-3',
|
||||
'font-sans leading-none',
|
||||
'bg-gray-50 dark:bg-gray-900/50',
|
||||
'group-active:bg-gray-50 group-active:dark:bg-gray-900/50',
|
||||
'group-hover:bg-gray-100 group-hover:dark:bg-gray-900/70',
|
||||
'border-t border-gray-200 dark:border-gray-800',
|
||||
)}>
|
||||
<div className="text-gray-800 dark:text-white font-medium">
|
||||
{titleForPhoto(photo)}
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
{ogImageDescriptionForPhoto(photo)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<OGTile {...{
|
||||
title: titleForPhoto(photo),
|
||||
description: descriptionForPhoto(photo),
|
||||
path: pathForPhoto(photo),
|
||||
pathImageAbsolute: absolutePathForPhotoImage(photo),
|
||||
loadingState: loadingStateExternal,
|
||||
onLoad,
|
||||
onFail,
|
||||
riseOnHover,
|
||||
retryTime,
|
||||
}}/>
|
||||
);
|
||||
};
|
||||
|
||||
22
src/photo/PhotoShareModal.tsx
Normal file
22
src/photo/PhotoShareModal.tsx
Normal file
@ -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 (
|
||||
<ShareModal
|
||||
title="Share Photo"
|
||||
pathShare={absolutePathForPhoto(photo, tag)}
|
||||
pathClose={pathForPhoto(photo, tag)}
|
||||
>
|
||||
<PhotoOGTile photo={photo} />
|
||||
</ShareModal>
|
||||
);
|
||||
};
|
||||
@ -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[]) => {
|
||||
|
||||
@ -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)}`;
|
||||
|
||||
|
||||
@ -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({
|
||||
)}>
|
||||
<PhotoTag tag={tag} />
|
||||
<span className={cc(
|
||||
'inline-flex gap-2 items-center self-start',
|
||||
'uppercase text-gray-400 dark:text-gray-500',
|
||||
'sm:col-span-2 md:col-span-1 lg:col-span-2',
|
||||
)}>
|
||||
{selectedPhotoIndex !== undefined
|
||||
? `Tagged ${selectedPhotoIndex + 1} of ${photos.length}`
|
||||
: descriptionForTaggedPhotos(photos)}
|
||||
{selectedPhotoIndex === undefined &&
|
||||
<ShareButton path={pathForTagShare(tag)} dim />}
|
||||
</span>
|
||||
<span className={cc(
|
||||
'hidden sm:inline-block',
|
||||
|
||||
38
src/tag/TagOGTile.tsx
Normal file
38
src/tag/TagOGTile.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { Photo } from '@/photo';
|
||||
import { absolutePathForTagImage, pathForTag } from '@/site/paths';
|
||||
import OGTile from '@/components/OGTile';
|
||||
import { descriptionForTaggedPhotos, titleForTag } from '.';
|
||||
|
||||
export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed';
|
||||
|
||||
export default function TagOGTile({
|
||||
tag,
|
||||
photos,
|
||||
loadingState: loadingStateExternal,
|
||||
riseOnHover,
|
||||
onLoad,
|
||||
onFail,
|
||||
retryTime,
|
||||
}: {
|
||||
tag: string
|
||||
photos: Photo[]
|
||||
loadingState?: OGLoadingState
|
||||
onLoad?: () => void
|
||||
onFail?: () => void
|
||||
riseOnHover?: boolean
|
||||
retryTime?: number
|
||||
}) {
|
||||
return (
|
||||
<OGTile {...{
|
||||
title: titleForTag(tag, photos),
|
||||
description: descriptionForTaggedPhotos(photos, true),
|
||||
path: pathForTag(tag),
|
||||
pathImageAbsolute: absolutePathForTagImage(tag),
|
||||
loadingState: loadingStateExternal,
|
||||
onLoad,
|
||||
onFail,
|
||||
riseOnHover,
|
||||
retryTime,
|
||||
}}/>
|
||||
);
|
||||
};
|
||||
22
src/tag/TagShareModal.tsx
Normal file
22
src/tag/TagShareModal.tsx
Normal file
@ -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 (
|
||||
<ShareModal
|
||||
title="Share Photos"
|
||||
pathShare={absolutePathForTag(tag)}
|
||||
pathClose={pathForTag(tag)}
|
||||
>
|
||||
<TagOGTile tag={tag} photos={photos} />
|
||||
</ShareModal>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user