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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ export default function PhotoGridContainer({
return ( return (
<SiteGrid <SiteGrid
contentMain={<div className={clsx( contentMain={<div className={clsx(
header && 'space-y-8 mt-4', header && 'space-y-8 mt-2',
)}> )}>
{header && {header &&
<AnimateItems <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'; 'use client';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Photo, titleForPhoto } from '@/photo'; import { Photo, PhotoSetAttributes, titleForPhoto } from '@/photo';
import Link from 'next/link'; import Link from 'next/link';
import { AnimationConfig } from '../components/AnimateItems'; import { AnimationConfig } from '../components/AnimateItems';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
import { pathForPhoto } from '@/site/paths'; import { pathForPhoto } from '@/site/paths';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
export default function PhotoLink({ export default function PhotoLink({
@ -23,16 +21,12 @@ export default function PhotoLink({
children, children,
}: { }: {
photo?: Photo photo?: Photo
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
scroll?: boolean scroll?: boolean
prefetch?: boolean prefetch?: boolean
nextPhotoAnimation?: AnimationConfig nextPhotoAnimation?: AnimationConfig
className?: string className?: string
children?: ReactNode children?: ReactNode
}) { } & PhotoSetAttributes) {
const { setNextPhotoAnimation } = useAppState(); const { setNextPhotoAnimation } = useAppState();
return ( return (

View File

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

View File

@ -1,16 +1,17 @@
'use client'; 'use client';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Photo, getNextPhoto, getPreviousPhoto } from '@/photo'; import {
Photo,
PhotoSetAttributes,
getNextPhoto,
getPreviousPhoto,
} from '@/photo';
import PhotoLink from './PhotoLink'; import PhotoLink from './PhotoLink';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { pathForPhoto } from '@/site/paths'; import { pathForPhoto } from '@/site/paths';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
import { AnimationConfig } from '@/components/AnimateItems'; 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'; import { clsx } from 'clsx/lite';
const LISTENER_KEYUP = 'keyup'; const LISTENER_KEYUP = 'keyup';
@ -18,25 +19,19 @@ const LISTENER_KEYUP = 'keyup';
const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 }; const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 };
const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 }; const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
export default function PhotoNav({ export default function PhotoPrevNext({
photo, photo,
photos, photos = [],
className, className,
tag, tag,
camera, camera,
simulation, simulation,
focal, focal,
prefetch,
}: { }: {
photo: Photo photo?: Photo
photos: Photo[] photos?: Photo[]
className?: string className?: string
tag?: string } & PhotoSetAttributes) {
camera?: Camera
simulation?: FilmSimulation
focal?: number
prefetch?: boolean
}) {
const router = useRouter(); const router = useRouter();
const { const {
@ -44,8 +39,8 @@ export default function PhotoNav({
shouldRespondToKeyboardCommands, shouldRespondToKeyboardCommands,
} = useAppState(); } = useAppState();
const previousPhoto = getPreviousPhoto(photo, photos); const previousPhoto = photo ? getPreviousPhoto(photo, photos) : undefined;
const nextPhoto = getNextPhoto(photo, photos); const nextPhoto = photo ? getNextPhoto(photo, photos) : undefined;
useEffect(() => { useEffect(() => {
if (shouldRespondToKeyboardCommands) { if (shouldRespondToKeyboardCommands) {
@ -105,8 +100,10 @@ export default function PhotoNav({
'flex items-center', 'flex items-center',
className, className,
)}> )}>
<div className="flex items-center gap-2">
<PhotoLink <PhotoLink
photo={previousPhoto} photo={previousPhoto}
className="select-none"
nextPhotoAnimation={ANIMATION_RIGHT} nextPhotoAnimation={ANIMATION_RIGHT}
tag={tag} tag={tag}
camera={camera} camera={camera}
@ -116,25 +113,13 @@ export default function PhotoNav({
prefetch prefetch
> >
<span className="group inline-flex gap-1 items-center"> <span className="group inline-flex gap-1 items-center">
<BiChevronLeft
className={clsx(
'text-[1.25rem] transition-transform',
'group-hover:-translate-x-1',
)}
/>
PREV PREV
</span> </span>
</PhotoLink> </PhotoLink>
<div className="grow text-center"> <span className="text-extra-extra-dim">/</span>
{(photo.title || SHOW_PHOTO_TITLE_FALLBACK_TEXT) &&
<PhotoLink
photo={photo}
className="uppercase font-bold"
prefetch={prefetch}
/>}
</div>
<PhotoLink <PhotoLink
photo={nextPhoto} photo={nextPhoto}
className="select-none"
nextPhotoAnimation={ANIMATION_LEFT} nextPhotoAnimation={ANIMATION_LEFT}
tag={tag} tag={tag}
camera={camera} camera={camera}
@ -145,14 +130,9 @@ export default function PhotoNav({
> >
<span className="group inline-flex gap-1 items-center"> <span className="group inline-flex gap-1 items-center">
NEXT NEXT
<BiChevronRight
className={clsx(
'text-[1.25rem] transition-transform',
'group-hover:translate-x-1',
)}
/>
</span> </span>
</PhotoLink> </PhotoLink>
</div> </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 PhotoOGTile from '@/photo/PhotoOGTile';
import { absolutePathForPhoto, pathForPhoto } from '@/site/paths'; import { absolutePathForPhoto, pathForPhoto } from '@/site/paths';
import { Photo } from '.'; import { Photo, PhotoSetAttributes } from '.';
import ShareModal from '@/components/ShareModal'; import ShareModal from '@/components/ShareModal';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
export default function PhotoShareModal(props: { export default function PhotoShareModal(props: {
photo: Photo photo: Photo
tag?: string } & PhotoSetAttributes) {
camera?: Camera
simulation?: FilmSimulation
focal?: number
}) {
return ( return (
<ShareModal <ShareModal
pathShare={absolutePathForPhoto(props)} 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 ImageSmall from '@/components/image/ImageSmall';
import Link from 'next/link'; import Link from 'next/link';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
@ -6,8 +11,6 @@ import { pathForPhoto } from '@/site/paths';
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config'; import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
import { useRef } from 'react'; import { useRef } from 'react';
import useOnVisible from '@/utility/useOnVisible'; import useOnVisible from '@/utility/useOnVisible';
import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation';
export default function PhotoSmall({ export default function PhotoSmall({
photo, photo,
@ -21,15 +24,11 @@ export default function PhotoSmall({
onVisible, onVisible,
}: { }: {
photo: Photo photo: Photo
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
selected?: boolean selected?: boolean
className?: string className?: string
prefetch?: boolean prefetch?: boolean
onVisible?: () => void onVisible?: () => void
}) { } & PhotoSetAttributes) {
const ref = useRef<HTMLAnchorElement>(null); const ref = useRef<HTMLAnchorElement>(null);
useOnVisible(ref, onVisible); 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 { PRIORITY_ORDER_ENABLED } from '@/site/config';
import { parameterize } from '@/utility/string'; import { parameterize } from '@/utility/string';
import { PhotoSetAttributes } from '..';
export const GENERATE_STATIC_PARAMS_LIMIT = 1000; export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
export const PHOTO_DEFAULT_LIMIT = 100; export const PHOTO_DEFAULT_LIMIT = 100;
@ -12,16 +10,11 @@ export type GetPhotosOptions = {
limit?: number limit?: number
offset?: number offset?: number
query?: string query?: string
tag?: string
camera?: Camera
lens?: Lens
simulation?: FilmSimulation
focal?: number
takenBefore?: Date takenBefore?: Date
takenAfterInclusive?: Date takenAfterInclusive?: Date
updatedBefore?: Date updatedBefore?: Date
hidden?: 'exclude' | 'include' | 'only' hidden?: 'exclude' | 'include' | 'only'
}; } & PhotoSetAttributes;
export const areOptionsSensitive = (options: GetPhotosOptions) => export const areOptionsSensitive = (options: GetPhotosOptions) =>
options.hidden === 'include' || options.hidden === 'only'; options.hidden === 'include' || options.hidden === 'only';

View File

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

View File

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

View File

@ -142,6 +142,10 @@
@apply @apply
text-gray-400/80 dark:text-gray-400/50 text-gray-400/80 dark:text-gray-400/50
} }
.text-extra-extra-dim {
@apply
text-gray-200 dark:text-gray-800
}
.text-icon { .text-icon {
@apply @apply
text-gray-800 dark:text-gray-200 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 { BASE_URL, GRID_HOMEPAGE_ENABLED } from './config';
import { Camera } from '@/camera'; import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation'; import { FilmSimulation } from '@/simulation';
@ -75,13 +75,7 @@ export const PATHS_TO_CACHE = [
...PATHS_ADMIN, ...PATHS_ADMIN,
]; ];
interface PhotoPathParams { type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetAttributes;
photo: PhotoOrPhotoId
tag?: string
camera?: Camera
simulation?: FilmSimulation
focal?: number
}
// Absolute paths // Absolute paths
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`; export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
@ -280,11 +274,7 @@ export const isPathProtected = (pathname?: string) =>
export const getPathComponents = (pathname = ''): { export const getPathComponents = (pathname = ''): {
photoId?: string photoId?: string
tag?: string } & PhotoSetAttributes => {
camera?: Camera
simulation?: FilmSimulation
focal?: number
} => {
const photoIdFromPhoto = pathname.match( const photoIdFromPhoto = pathname.match(
new RegExp(`^${PREFIX_PHOTO}/([^/]+)`))?.[1]; new RegExp(`^${PREFIX_PHOTO}/([^/]+)`))?.[1];
const photoIdFromTag = pathname.match( const photoIdFromTag = pathname.match(

View File

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

View File

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