Combine photo nav + sets

This commit is contained in:
Sam Becker 2024-08-31 19:43:52 -05:00
parent e0a83415b0
commit db77448a63
20 changed files with 281 additions and 315 deletions

View File

@ -1,6 +1,6 @@
import { Photo, PhotoDateRange } from '@/photo';
import { pathForCameraShare } from '@/site/paths';
import PhotoSetHeader from '@/photo/PhotoSetHeader';
import PhotoHeader from '@/photo/PhotoHeader';
import { Camera, cameraFromPhoto } from '.';
import PhotoCamera from './PhotoCamera';
import { descriptionForCameraPhotos } from './meta';
@ -22,7 +22,8 @@ export default function CameraHeader({
}) {
const camera = cameraFromPhoto(photos[0], cameraProp);
return (
<PhotoSetHeader
<PhotoHeader
camera={camera}
entity={<PhotoCamera {...{ camera }} contrast="high" hideAppleIcon />}
entityVerb="Photo"
entityDescription={

View File

@ -1,7 +1,7 @@
import { Photo, PhotoDateRange } from '@/photo';
import { descriptionForFocalLengthPhotos } from '.';
import { pathForFocalLengthShare } from '@/site/paths';
import PhotoSetHeader from '@/photo/PhotoSetHeader';
import PhotoHeader from '@/photo/PhotoHeader';
import PhotoFocalLength from './PhotoFocalLength';
export default function FocalLengthHeader({
@ -20,7 +20,8 @@ export default function FocalLengthHeader({
dateRange?: PhotoDateRange
}) {
return (
<PhotoSetHeader
<PhotoHeader
focal={focal}
entity={<PhotoFocalLength focal={focal} contrast="high" />}
entityDescription={descriptionForFocalLengthPhotos(
photos,

View File

@ -10,11 +10,9 @@ import {
import SiteGrid from '@/components/SiteGrid';
import Spinner from '@/components/Spinner';
import { getPhotosCachedAction, getPhotosAction } from '@/photo/actions';
import { Photo } from '.';
import { Photo, PhotoSetAttributes } from '.';
import { clsx } from 'clsx/lite';
import { useAppState } from '@/state/AppState';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
import { GetPhotosOptions } from './db';
export type RevalidatePhoto = (
@ -38,9 +36,6 @@ export default function InfinitePhotoScroll({
initialOffset: number
itemsPerPage: number
sortBy?: GetPhotosOptions['sortBy']
tag?: string
camera?: Camera
simulation?: FilmSimulation
cacheKey: string
wrapMoreButtonInGrid?: boolean
useCachedPhotos?: boolean
@ -50,7 +45,7 @@ export default function InfinitePhotoScroll({
onLastPhotoVisible: () => void
revalidatePhoto?: RevalidatePhoto
}) => ReactNode
}) {
} & PhotoSetAttributes) {
const { swrTimestamp, isUserSignedIn } = useAppState();
const key = `${swrTimestamp}-${cacheKey}`;

View File

@ -1,17 +1,15 @@
import AnimateItems from '@/components/AnimateItems';
import { Photo, PhotoDateRange } from '.';
import { Photo, PhotoDateRange, PhotoSetAttributes } from '.';
import PhotoLarge from './PhotoLarge';
import SiteGrid from '@/components/SiteGrid';
import PhotoGrid from './PhotoGrid';
import PhotoNav from './PhotoNav';
import TagHeader from '@/tag/TagHeader';
import { Camera } from '@/camera';
import CameraHeader from '@/camera/CameraHeader';
import { FilmSimulation } from '@/simulation';
import FilmSimulationHeader from '@/simulation/FilmSimulationHeader';
import { TAG_HIDDEN } from '@/tag';
import HiddenHeader from '@/tag/HiddenHeader';
import FocalLengthHeader from '@/focal/FocalLengthHeader';
import PhotoHeader from './PhotoHeader';
export default function PhotoDetailPage({
photo,
@ -30,22 +28,16 @@ export default function PhotoDetailPage({
photo: Photo
photos: Photo[]
photosGrid?: Photo[]
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
indexNumber?: number
count?: number
dateRange?: PhotoDateRange
shouldShare?: boolean
includeFavoriteInAdminMenu?: boolean
}) {
return (
<div>
{tag &&
<SiteGrid
className="mt-4 mb-8"
contentMain={tag === TAG_HIDDEN
} & PhotoSetAttributes) {
let customHeader: JSX.Element | undefined;
if (tag) {
customHeader = tag === TAG_HIDDEN
? <HiddenHeader
photos={photos}
selectedPhoto={photo}
@ -60,64 +52,44 @@ export default function PhotoDetailPage({
indexNumber={indexNumber}
count={count}
dateRange={dateRange}
/>}
/>}
{camera &&
<SiteGrid
className="mt-4 mb-8"
contentMain={
<CameraHeader
/>;
} else if (camera) {
customHeader = <CameraHeader
camera={camera}
photos={photos}
selectedPhoto={photo}
indexNumber={indexNumber}
count={count}
dateRange={dateRange}
/>}
/>}
{simulation &&
<SiteGrid
className="mt-4 mb-8"
contentMain={
<FilmSimulationHeader
/>;
} else if (simulation) {
customHeader = <FilmSimulationHeader
simulation={simulation}
photos={photos}
selectedPhoto={photo}
indexNumber={indexNumber}
count={count}
dateRange={dateRange}
/>}
/>}
{focal &&
<SiteGrid
className="mt-4 mb-8"
contentMain={
<FocalLengthHeader
/>;
} else if (focal) {
customHeader = <FocalLengthHeader
focal={focal}
photos={photos}
selectedPhoto={photo}
indexNumber={indexNumber}
count={count}
dateRange={dateRange}
/>}
/>}
<AnimateItems
animateOnFirstLoadOnly
items={[
/>;
}
return (
<div>
<SiteGrid
key="photo-nav"
className="mb-4"
contentMain={<PhotoNav {...{
photo,
photos,
className: 'border-t pt-4 border-gray-100 dark:border-gray-900',
tag,
camera,
simulation,
focal,
}} />}
/>,
]}
className="mt-2 mb-6 sm:mb-8"
contentMain={customHeader ?? <PhotoHeader
selectedPhoto={photo}
photos={photos}
/>}
/>
<AnimateItems
className="md:mb-8"
@ -129,7 +101,7 @@ export default function PhotoDetailPage({
primaryTag={tag}
priority
prefetchRelatedLinks
showTitle={false}
showTitle={Boolean(customHeader)}
showCamera={!camera}
showSimulation={!simulation}
shouldShare={shouldShare}

View File

@ -1,11 +1,9 @@
'use client';
import { Photo } from '.';
import { Photo, PhotoSetAttributes } from '.';
import PhotoMedium from './PhotoMedium';
import { clsx } from 'clsx/lite';
import AnimateItems from '@/components/AnimateItems';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
import { GRID_ASPECT_RATIO, HIGH_DENSITY_GRID } from '@/site/config';
import { useAppState } from '@/state/AppState';
import SelectTileOverlay from '@/components/SelectTileOverlay';
@ -31,10 +29,6 @@ export default function PhotoGrid({
}: {
photos: Photo[]
selectedPhoto?: Photo
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
photoPriority?: boolean
fast?: boolean
animate?: boolean
@ -46,7 +40,7 @@ export default function PhotoGrid({
canSelect?: boolean
onLastPhotoVisible?: () => void
onAnimationComplete?: () => void
}) {
} & PhotoSetAttributes) {
const {
isUserSignedIn,
selectedPhotoIds,

View File

@ -38,7 +38,7 @@ export default function PhotoGridContainer({
return (
<SiteGrid
contentMain={<div className={clsx(
header && 'space-y-8 mt-4',
header && 'space-y-8 mt-2',
)}>
{header &&
<AnimateItems

123
src/photo/PhotoHeader.tsx Normal file
View File

@ -0,0 +1,123 @@
import { clsx } from 'clsx/lite';
import {
Photo,
PhotoDateRange,
PhotoSetAttributes,
dateRangeForPhotos,
} from '.';
import ShareButton from '@/components/ShareButton';
import AnimateItems from '@/components/AnimateItems';
import { ReactNode } from 'react';
import {
HIGH_DENSITY_GRID,
SHOW_PHOTO_TITLE_FALLBACK_TEXT,
} from '@/site/config';
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
import PhotoPrevNext from './PhotoPrevNext';
import PhotoLink from './PhotoLink';
export default function PhotoHeader({
tag,
camera,
simulation,
focal,
photos,
selectedPhoto,
entity,
entityVerb,
entityDescription,
sharePath,
indexNumber,
count,
dateRange,
}: {
photos: Photo[]
selectedPhoto?: Photo
entity?: ReactNode
entityVerb?: string
entityDescription?: string
sharePath?: string
indexNumber?: number
count?: number
dateRange?: PhotoDateRange
} & PhotoSetAttributes) {
const { start, end } = dateRangeForPhotos(photos, dateRange);
const selectedPhotoIndex = selectedPhoto
? photos.findIndex(photo => photo.id === selectedPhoto.id)
: undefined;
const renderPrevNext = () =>
<PhotoPrevNext {...{
photo: selectedPhoto,
photos,
tag,
camera,
simulation,
focal,
}} />;
const renderDateRange = () =>
<span className="text-dim uppercase text-right">
{start === end
? start
: <>{end}<br /> {start}</>}
</span>;
return (
<AnimateItems
type="bottom"
distanceOffset={10}
animateOnFirstLoadOnly
items={[<DivDebugBaselineGrid
key="PhotosHeader"
className={clsx(
'grid gap-0.5 sm:gap-1 items-start grid-cols-2',
HIGH_DENSITY_GRID
? 'sm:grid-cols-4 lg:grid-cols-5'
: 'sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
)}>
<span className={clsx(
'inline-flex uppercase',
HIGH_DENSITY_GRID && 'sm:col-span-2',
)}>
{entity ?? (
(selectedPhoto?.title || SHOW_PHOTO_TITLE_FALLBACK_TEXT)
? <PhotoLink
photo={selectedPhoto}
className="uppercase font-bold"
/>
: <>X of X</>
)}
</span>
<span className={clsx(
'hidden sm:block',
'inline-flex gap-2 self-start',
'uppercase text-dim',
HIGH_DENSITY_GRID
? 'lg:col-span-2'
: 'sm:col-span-2 md:col-span-1 lg:col-span-2',
)}>
{entity && <>
{selectedPhotoIndex !== undefined
// eslint-disable-next-line max-len
? `${entityVerb ? `${entityVerb} ` : ''}${indexNumber || (selectedPhotoIndex + 1)} of ${count ?? photos.length}`
: entityDescription}
{selectedPhotoIndex === undefined && sharePath &&
<ShareButton
className="translate-y-[1.5px]"
path={sharePath}
dim
/>}
</>}
</span>
<div className="flex justify-end">
{selectedPhoto
? renderPrevNext()
: renderDateRange()}
</div>
</DivDebugBaselineGrid>,
]}
/>
);
}

View File

@ -1,13 +1,11 @@
'use client';
import { ReactNode } from 'react';
import { Photo, titleForPhoto } from '@/photo';
import { Photo, PhotoSetAttributes, titleForPhoto } from '@/photo';
import Link from 'next/link';
import { AnimationConfig } from '../components/AnimateItems';
import { useAppState } from '@/state/AppState';
import { pathForPhoto } from '@/site/paths';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
import { clsx } from 'clsx/lite';
export default function PhotoLink({
@ -23,16 +21,12 @@ export default function PhotoLink({
children,
}: {
photo?: Photo
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
scroll?: boolean
prefetch?: boolean
nextPhotoAnimation?: AnimationConfig
className?: string
children?: ReactNode
}) {
} & PhotoSetAttributes) {
const { setNextPhotoAnimation } = useAppState();
return (

View File

@ -1,12 +1,15 @@
'use client';
import { Photo, altTextForPhoto, doesPhotoNeedBlurCompatibility } from '.';
import {
Photo,
PhotoSetAttributes,
altTextForPhoto,
doesPhotoNeedBlurCompatibility,
} from '.';
import ImageMedium from '@/components/image/ImageMedium';
import Link from 'next/link';
import { clsx } from 'clsx/lite';
import { pathForPhoto } from '@/site/paths';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
import { useRef } from 'react';
import useOnVisible from '@/utility/useOnVisible';
@ -24,16 +27,12 @@ export default function PhotoMedium({
onVisible,
}: {
photo: Photo
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
selected?: boolean
priority?: boolean
prefetch?: boolean
className?: string
onVisible?: () => void
}) {
} & PhotoSetAttributes) {
const ref = useRef<HTMLAnchorElement>(null);
useOnVisible(ref, onVisible);

View File

@ -1,16 +1,17 @@
'use client';
import { useEffect } from 'react';
import { Photo, getNextPhoto, getPreviousPhoto } from '@/photo';
import {
Photo,
PhotoSetAttributes,
getNextPhoto,
getPreviousPhoto,
} from '@/photo';
import PhotoLink from './PhotoLink';
import { useRouter } from 'next/navigation';
import { pathForPhoto } from '@/site/paths';
import { useAppState } from '@/state/AppState';
import { AnimationConfig } from '@/components/AnimateItems';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
import { SHOW_PHOTO_TITLE_FALLBACK_TEXT } from '@/site/config';
import { BiChevronLeft, BiChevronRight } from 'react-icons/bi';
import { clsx } from 'clsx/lite';
const LISTENER_KEYUP = 'keyup';
@ -18,25 +19,19 @@ const LISTENER_KEYUP = 'keyup';
const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 };
const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
export default function PhotoNav({
export default function PhotoPrevNext({
photo,
photos,
photos = [],
className,
tag,
camera,
simulation,
focal,
prefetch,
}: {
photo: Photo
photos: Photo[]
photo?: Photo
photos?: Photo[]
className?: string
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
prefetch?: boolean
}) {
} & PhotoSetAttributes) {
const router = useRouter();
const {
@ -44,8 +39,8 @@ export default function PhotoNav({
shouldRespondToKeyboardCommands,
} = useAppState();
const previousPhoto = getPreviousPhoto(photo, photos);
const nextPhoto = getNextPhoto(photo, photos);
const previousPhoto = photo ? getPreviousPhoto(photo, photos) : undefined;
const nextPhoto = photo ? getNextPhoto(photo, photos) : undefined;
useEffect(() => {
if (shouldRespondToKeyboardCommands) {
@ -105,8 +100,10 @@ export default function PhotoNav({
'flex items-center',
className,
)}>
<div className="flex items-center gap-2">
<PhotoLink
photo={previousPhoto}
className="select-none"
nextPhotoAnimation={ANIMATION_RIGHT}
tag={tag}
camera={camera}
@ -116,25 +113,13 @@ export default function PhotoNav({
prefetch
>
<span className="group inline-flex gap-1 items-center">
<BiChevronLeft
className={clsx(
'text-[1.25rem] transition-transform',
'group-hover:-translate-x-1',
)}
/>
PREV
</span>
</PhotoLink>
<div className="grow text-center">
{(photo.title || SHOW_PHOTO_TITLE_FALLBACK_TEXT) &&
<PhotoLink
photo={photo}
className="uppercase font-bold"
prefetch={prefetch}
/>}
</div>
<span className="text-extra-extra-dim">/</span>
<PhotoLink
photo={nextPhoto}
className="select-none"
nextPhotoAnimation={ANIMATION_LEFT}
tag={tag}
camera={camera}
@ -145,14 +130,9 @@ export default function PhotoNav({
>
<span className="group inline-flex gap-1 items-center">
NEXT
<BiChevronRight
className={clsx(
'text-[1.25rem] transition-transform',
'group-hover:translate-x-1',
)}
/>
</span>
</PhotoLink>
</div>
</div>
);
};

View File

@ -1,85 +0,0 @@
import { clsx } from 'clsx/lite';
import { Photo, PhotoDateRange, dateRangeForPhotos } from '.';
import ShareButton from '@/components/ShareButton';
import AnimateItems from '@/components/AnimateItems';
import { ReactNode } from 'react';
import { HIGH_DENSITY_GRID } from '@/site/config';
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
export default function PhotoSetHeader({
entity,
entityVerb,
entityDescription,
photos,
selectedPhoto,
sharePath,
indexNumber,
count,
dateRange,
}: {
entity: ReactNode
entityVerb?: string
entityDescription: string
photos: Photo[]
selectedPhoto?: Photo
sharePath?: string
indexNumber?: number
count?: number
dateRange?: PhotoDateRange
}) {
const { start, end } = dateRangeForPhotos(photos, dateRange);
const selectedPhotoIndex = selectedPhoto
? photos.findIndex(photo => photo.id === selectedPhoto.id)
: undefined;
return (
<AnimateItems
type="bottom"
distanceOffset={10}
animateOnFirstLoadOnly
items={[<DivDebugBaselineGrid
key="PhotosHeader"
className={clsx(
'grid gap-0.5 sm:gap-1 items-start',
HIGH_DENSITY_GRID
? 'xs:grid-cols-2 sm:grid-cols-4 lg:grid-cols-5'
: 'xs:grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
)}>
<span className={clsx(
'inline-flex uppercase',
HIGH_DENSITY_GRID && 'sm:col-span-2',
)}>
{entity}
</span>
<span className={clsx(
'inline-flex gap-2 self-start',
'uppercase text-dim',
HIGH_DENSITY_GRID
? 'lg:col-span-2'
: 'sm:col-span-2 md:col-span-1 lg:col-span-2',
)}>
{selectedPhotoIndex !== undefined
// eslint-disable-next-line max-len
? `${entityVerb ? `${entityVerb} ` : ''}${indexNumber || (selectedPhotoIndex + 1)} of ${count ?? photos.length}`
: entityDescription}
{selectedPhotoIndex === undefined && sharePath &&
<ShareButton
className="translate-y-[1.5px]"
path={sharePath}
dim
/>}
</span>
<span className={clsx(
'hidden sm:inline-block',
'text-right uppercase',
'text-dim',
)}>
{start === end
? start
: <>{end}<br /> {start}</>}
</span>
</DivDebugBaselineGrid>]}
/>
);
}

View File

@ -1,17 +1,11 @@
import PhotoOGTile from '@/photo/PhotoOGTile';
import { absolutePathForPhoto, pathForPhoto } from '@/site/paths';
import { Photo } from '.';
import { Photo, PhotoSetAttributes } from '.';
import ShareModal from '@/components/ShareModal';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
export default function PhotoShareModal(props: {
photo: Photo
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
}) {
} & PhotoSetAttributes) {
return (
<ShareModal
pathShare={absolutePathForPhoto(props)}

View File

@ -1,4 +1,9 @@
import { Photo, altTextForPhoto, doesPhotoNeedBlurCompatibility } from '.';
import {
Photo,
PhotoSetAttributes,
altTextForPhoto,
doesPhotoNeedBlurCompatibility,
} from '.';
import ImageSmall from '@/components/image/ImageSmall';
import Link from 'next/link';
import { clsx } from 'clsx/lite';
@ -6,8 +11,6 @@ import { pathForPhoto } from '@/site/paths';
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
import { useRef } from 'react';
import useOnVisible from '@/utility/useOnVisible';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
export default function PhotoSmall({
photo,
@ -21,15 +24,11 @@ export default function PhotoSmall({
onVisible,
}: {
photo: Photo
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
selected?: boolean
className?: string
prefetch?: boolean
onVisible?: () => void
}) {
} & PhotoSetAttributes) {
const ref = useRef<HTMLAnchorElement>(null);
useOnVisible(ref, onVisible);

View File

@ -1,8 +1,6 @@
import { Camera } from '@/camera';
import { Lens } from '@/lens';
import { FilmSimulation } from '@/simulation';
import { PRIORITY_ORDER_ENABLED } from '@/site/config';
import { parameterize } from '@/utility/string';
import { PhotoSetAttributes } from '..';
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
export const PHOTO_DEFAULT_LIMIT = 100;
@ -12,16 +10,11 @@ export type GetPhotosOptions = {
limit?: number
offset?: number
query?: string
tag?: string
camera?: Camera
lens?: Lens
simulation?: FilmSimulation
focal?: number
takenBefore?: Date
takenAfterInclusive?: Date
updatedBefore?: Date
hidden?: 'exclude' | 'include' | 'only'
};
} & PhotoSetAttributes;
export const areOptionsSensitive = (options: GetPhotosOptions) =>
options.hidden === 'include' || options.hidden === 'only';

View File

@ -1,4 +1,6 @@
import { Camera } from '@/camera';
import { formatFocalLength } from '@/focal';
import { Lens } from '@/lens';
import { getNextImageUrlForRequest } from '@/services/next-image';
import { FilmSimulation } from '@/simulation';
import { HIGH_DENSITY_GRID, SHOW_EXIF_DATA } from '@/site/config';
@ -99,6 +101,14 @@ export interface Photo extends PhotoDb {
takenAtNaiveFormatted: string
}
export interface PhotoSetAttributes {
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
lens?: Lens // Unimplemented as a set
}
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
const photoDb = camelcaseKeys(
photoDbRaw as unknown as Record<string, unknown>

View File

@ -1,7 +1,7 @@
import { Photo, PhotoDateRange } from '@/photo';
import { FilmSimulation, descriptionForFilmSimulationPhotos } from '.';
import { pathForFilmSimulationShare } from '@/site/paths';
import PhotoSetHeader from '@/photo/PhotoSetHeader';
import PhotoHeader from '@/photo/PhotoHeader';
import PhotoFilmSimulation from
'@/simulation/PhotoFilmSimulation';
@ -21,7 +21,8 @@ export default function FilmSimulationHeader({
dateRange?: PhotoDateRange
}) {
return (
<PhotoSetHeader
<PhotoHeader
simulation={simulation}
entity={<PhotoFilmSimulation {...{ simulation }} />}
entityVerb="Photo"
entityDescription={descriptionForFilmSimulationPhotos(

View File

@ -142,6 +142,10 @@
@apply
text-gray-400/80 dark:text-gray-400/50
}
.text-extra-extra-dim {
@apply
text-gray-200 dark:text-gray-800
}
.text-icon {
@apply
text-gray-800 dark:text-gray-200

View File

@ -1,4 +1,4 @@
import { Photo } from '@/photo';
import { Photo, PhotoSetAttributes } from '@/photo';
import { BASE_URL, GRID_HOMEPAGE_ENABLED } from './config';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
@ -75,13 +75,7 @@ export const PATHS_TO_CACHE = [
...PATHS_ADMIN,
];
interface PhotoPathParams {
photo: PhotoOrPhotoId
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
}
type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetAttributes;
// Absolute paths
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
@ -280,11 +274,7 @@ export const isPathProtected = (pathname?: string) =>
export const getPathComponents = (pathname = ''): {
photoId?: string
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
} => {
} & PhotoSetAttributes => {
const photoIdFromPhoto = pathname.match(
new RegExp(`^${PREFIX_PHOTO}/([^/]+)`))?.[1];
const photoIdFromTag = pathname.match(

View File

@ -1,5 +1,5 @@
import { Photo, photoQuantityText } from '@/photo';
import PhotoSetHeader from '@/photo/PhotoSetHeader';
import PhotoHeader from '@/photo/PhotoHeader';
import HiddenTag from './HiddenTag';
export default function HiddenHeader({
@ -14,7 +14,7 @@ export default function HiddenHeader({
count: number
}) {
return (
<PhotoSetHeader
<PhotoHeader
key="HiddenHeader"
entity={<HiddenTag contrast="high" />}
entityDescription={photoQuantityText(count, false)}

View File

@ -2,7 +2,7 @@ import { Photo, PhotoDateRange } from '@/photo';
import PhotoTag from './PhotoTag';
import { descriptionForTaggedPhotos, isTagFavs } from '.';
import { pathForTagShare } from '@/site/paths';
import PhotoSetHeader from '@/photo/PhotoSetHeader';
import PhotoHeader from '@/photo/PhotoHeader';
import FavsTag from './FavsTag';
export default function TagHeader({
@ -21,7 +21,8 @@ export default function TagHeader({
dateRange?: PhotoDateRange
}) {
return (
<PhotoSetHeader
<PhotoHeader
tag={tag}
entity={isTagFavs(tag)
? <FavsTag contrast="high" />
: <PhotoTag tag={tag} contrast="high" />}