Combine photo nav + sets
This commit is contained in:
parent
e0a83415b0
commit
db77448a63
@ -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={
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}`;
|
||||
|
||||
@ -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,94 +28,68 @@ 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
|
||||
}) {
|
||||
} & PhotoSetAttributes) {
|
||||
let customHeader: JSX.Element | undefined;
|
||||
|
||||
if (tag) {
|
||||
customHeader = tag === TAG_HIDDEN
|
||||
? <HiddenHeader
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count ?? 0}
|
||||
/>
|
||||
: <TagHeader
|
||||
key={tag}
|
||||
tag={tag}
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
/>;
|
||||
} else if (camera) {
|
||||
customHeader = <CameraHeader
|
||||
camera={camera}
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
/>;
|
||||
} else if (simulation) {
|
||||
customHeader = <FilmSimulationHeader
|
||||
simulation={simulation}
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
/>;
|
||||
} else if (focal) {
|
||||
customHeader = <FocalLengthHeader
|
||||
focal={focal}
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tag &&
|
||||
<SiteGrid
|
||||
className="mt-4 mb-8"
|
||||
contentMain={tag === TAG_HIDDEN
|
||||
? <HiddenHeader
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count ?? 0}
|
||||
/>
|
||||
: <TagHeader
|
||||
key={tag}
|
||||
tag={tag}
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
/>}
|
||||
<SiteGrid
|
||||
className="mt-2 mb-6 sm:mb-8"
|
||||
contentMain={customHeader ?? <PhotoHeader
|
||||
selectedPhoto={photo}
|
||||
photos={photos}
|
||||
/>}
|
||||
{camera &&
|
||||
<SiteGrid
|
||||
className="mt-4 mb-8"
|
||||
contentMain={
|
||||
<CameraHeader
|
||||
camera={camera}
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
/>}
|
||||
/>}
|
||||
{simulation &&
|
||||
<SiteGrid
|
||||
className="mt-4 mb-8"
|
||||
contentMain={
|
||||
<FilmSimulationHeader
|
||||
simulation={simulation}
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
/>}
|
||||
/>}
|
||||
{focal &&
|
||||
<SiteGrid
|
||||
className="mt-4 mb-8"
|
||||
contentMain={
|
||||
<FocalLengthHeader
|
||||
focal={focal}
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
/>}
|
||||
/>}
|
||||
<AnimateItems
|
||||
animateOnFirstLoadOnly
|
||||
items={[
|
||||
<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,
|
||||
}} />}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<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}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
123
src/photo/PhotoHeader.tsx
Normal 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>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,54 +100,39 @@ export default function PhotoNav({
|
||||
'flex items-center',
|
||||
className,
|
||||
)}>
|
||||
<PhotoLink
|
||||
photo={previousPhoto}
|
||||
nextPhotoAnimation={ANIMATION_RIGHT}
|
||||
tag={tag}
|
||||
camera={camera}
|
||||
simulation={simulation}
|
||||
focal={focal}
|
||||
scroll={false}
|
||||
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 className="flex items-center gap-2">
|
||||
<PhotoLink
|
||||
photo={previousPhoto}
|
||||
className="select-none"
|
||||
nextPhotoAnimation={ANIMATION_RIGHT}
|
||||
tag={tag}
|
||||
camera={camera}
|
||||
simulation={simulation}
|
||||
focal={focal}
|
||||
scroll={false}
|
||||
prefetch
|
||||
>
|
||||
<span className="group inline-flex gap-1 items-center">
|
||||
PREV
|
||||
</span>
|
||||
</PhotoLink>
|
||||
<span className="text-extra-extra-dim">/</span>
|
||||
<PhotoLink
|
||||
photo={nextPhoto}
|
||||
className="select-none"
|
||||
nextPhotoAnimation={ANIMATION_LEFT}
|
||||
tag={tag}
|
||||
camera={camera}
|
||||
simulation={simulation}
|
||||
focal={focal}
|
||||
scroll={false}
|
||||
prefetch
|
||||
>
|
||||
<span className="group inline-flex gap-1 items-center">
|
||||
NEXT
|
||||
</span>
|
||||
</PhotoLink>
|
||||
</div>
|
||||
<PhotoLink
|
||||
photo={nextPhoto}
|
||||
nextPhotoAnimation={ANIMATION_LEFT}
|
||||
tag={tag}
|
||||
camera={camera}
|
||||
simulation={simulation}
|
||||
focal={focal}
|
||||
scroll={false}
|
||||
prefetch
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@ -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>]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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)}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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" />}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user