Vercel/src/photo/PhotoHeader.tsx
2025-03-18 09:27:35 -05:00

175 lines
5.0 KiB
TypeScript

'use client';
import { clsx } from 'clsx/lite';
import {
Photo,
PhotoDateRange,
dateRangeForPhotos,
titleForPhoto,
} from '.';
import { PhotoSetCategory } from '../category';
import ShareButton from '@/share/ShareButton';
import AnimateItems from '@/components/AnimateItems';
import { ReactNode } from 'react';
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
import PhotoPrevNext from './PhotoPrevNext';
import PhotoLink from './PhotoLink';
import ResponsiveText from '@/components/primitives/ResponsiveText';
import { useAppState } from '@/state/AppState';
import { GRID_GAP_CLASSNAME } from '@/components';
export default function PhotoHeader({
photos,
selectedPhoto,
entity,
entityVerb = 'PHOTO',
entityDescription,
indexNumber,
count,
dateRange,
includeShareButton,
...categories
}: {
photos: Photo[]
selectedPhoto?: Photo
entity?: ReactNode
entityVerb?: string
entityDescription?: string
indexNumber?: number
count?: number
dateRange?: PhotoDateRange
includeShareButton?: boolean
} & PhotoSetCategory) {
const { isGridHighDensity } = useAppState();
const { start, end } = dateRangeForPhotos(photos, dateRange);
const selectedPhotoIndex = selectedPhoto
? photos.findIndex(photo => photo.id === selectedPhoto.id)
: undefined;
const paginationLabel =
(indexNumber || (selectedPhotoIndex ?? 0 + 1)) + ' of ' +
(count ?? photos.length);
const headerType = selectedPhotoIndex === undefined
? 'photo-set'
: entity
? 'photo-detail-with-entity'
: 'photo-detail';
const renderPrevNext =
<PhotoPrevNext {...{
photo: selectedPhoto,
photos,
...categories,
}} />;
const renderDateRange =
<span className="text-dim uppercase text-right">
{start === end
? start
: <>{end}<br />&ndash; {start}</>}
</span>;
const renderContentA = entity ?? (
selectedPhoto !== undefined &&
<PhotoLink
photo={selectedPhoto}
className="uppercase font-bold truncate"
>
{titleForPhoto(selectedPhoto, true)}
</PhotoLink>);
return (
<AnimateItems
type="bottom"
distanceOffset={10}
animateOnFirstLoadOnly
items={[<DivDebugBaselineGrid
key="PhotosHeader"
className={clsx(
'grid',
GRID_GAP_CLASSNAME,
'items-start',
'grid-cols-4',
isGridHighDensity
? 'lg:grid-cols-6'
: 'md:grid-cols-3 lg:grid-cols-4',
)}>
{/* Content A: Filter Set or Photo Title */}
<div className={clsx(
'inline-flex uppercase',
headerType === 'photo-set'
? isGridHighDensity
? 'col-span-2 sm:col-span-1 lg:col-span-2'
: 'col-span-2 sm:col-span-1'
: headerType === 'photo-detail-with-entity'
? isGridHighDensity
? 'col-span-2 sm:col-span-1 lg:col-span-2'
: 'col-span-2 sm:col-span-1'
: isGridHighDensity
? 'col-span-3 sm:col-span-3 lg:col-span-5 w-[110%] xl:w-full'
: 'col-span-3 md:col-span-2 lg:col-span-3 w-[110%] xl:w-full',
)}>
{headerType === 'photo-detail-with-entity'
? renderContentA
// Necessary for title truncation
: <h1 className={clsx(
'w-full truncate',
headerType !== 'photo-detail' && 'pr-1 sm:pr-2',
)}>
{renderContentA}
</h1>}
</div>
{/* Content B: Filter Set Meta or Photo Pagination */}
<div className={clsx(
'inline-flex gap-2 self-start',
'uppercase text-dim',
headerType === 'photo-set'
? isGridHighDensity
? 'col-span-2 lg:col-span-3'
: 'col-span-2 md:col-span-1 lg:col-span-2'
: headerType === 'photo-detail-with-entity'
? isGridHighDensity
? 'sm:col-span-2 lg:col-span-3'
: 'sm:col-span-2 md:col-span-1 lg:col-span-2'
: 'hidden!',
)}>
{entity && <>
{headerType === 'photo-set'
? <>
{entityDescription}
{includeShareButton &&
<ShareButton {...{
photos,
...categories,
count,
dateRange,
className: 'translate-y-[1.5px]',
prefetch: true,
dim: true,
}} />}
</>
: <ResponsiveText shortText={paginationLabel}>
{entityVerb} {paginationLabel}
</ResponsiveText>}
</>}
</div>
{/* Content C: Nav */}
<div className={clsx(
headerType === 'photo-set'
? 'hidden sm:flex'
: 'flex',
'justify-end',
)}>
{selectedPhoto
? renderPrevNext
: renderDateRange}
</div>
</DivDebugBaselineGrid>,
]}
/>
);
}