Vercel/src/photo/PhotoGrid.tsx
2025-10-05 10:56:11 -05:00

128 lines
3.8 KiB
TypeScript

'use client';
import { Photo } from '.';
import { PhotoSetCategory } from '../category';
import PhotoMedium from './PhotoMedium';
import { clsx } from 'clsx/lite';
import AnimateItems from '@/components/AnimateItems';
import { GRID_ASPECT_RATIO } from '@/app/config';
import { useAppState } from '@/app/AppState';
import SelectTileOverlay from '@/components/SelectTileOverlay';
import { ReactNode } from 'react';
import { GRID_GAP_CLASSNAME } from '@/components';
import { useSelectPhotosState } from '@/admin/select/SelectPhotosState';
import { DATA_KEY_PHOTO_GRID } from '@/admin/select/SelectPhotosProvider';
export default function PhotoGrid({
photos,
selectedPhoto,
prioritizeInitialPhotos,
className,
classNamePhoto,
animate = true,
canStart,
animateOnFirstLoadOnly,
staggerOnFirstLoadOnly = true,
additionalTile,
small,
selectable = true,
onLastPhotoVisible,
onAnimationComplete,
...categories
}: {
photos: Photo[]
selectedPhoto?: Photo
prioritizeInitialPhotos?: boolean
className?: string
classNamePhoto?: string
animate?: boolean
canStart?: boolean
animateOnFirstLoadOnly?: boolean
staggerOnFirstLoadOnly?: boolean
additionalTile?: ReactNode
small?: boolean
selectable?: boolean
onLastPhotoVisible?: () => void
onAnimationComplete?: () => void
} & PhotoSetCategory) {
const {
isGridHighDensity,
} = useAppState();
const {
isSelectingPhotos,
selectedPhotoIds,
setSelectedPhotoIds,
} = useSelectPhotosState();
return (
<div
{...{ [DATA_KEY_PHOTO_GRID]: selectable, className }}
>
<AnimateItems
className={clsx(
'grid',
GRID_GAP_CLASSNAME,
small
? 'grid-cols-3 xs:grid-cols-6'
: isGridHighDensity
? 'grid-cols-2 xs:grid-cols-4 lg:grid-cols-6'
: 'grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
'items-center',
)}
type={animate === false ? 'none' : undefined}
canStart={canStart}
duration={0.7}
staggerDelay={0.04}
distanceOffset={40}
animateOnFirstLoadOnly={animateOnFirstLoadOnly}
staggerOnFirstLoadOnly={staggerOnFirstLoadOnly}
onAnimationComplete={onAnimationComplete}
items={photos.map((photo, index) => {
const isSelected = selectedPhotoIds?.includes(photo.id) ?? false;
return <div
key={photo.id}
className={clsx(
'flex relative overflow-hidden',
'group',
)}
style={{
...GRID_ASPECT_RATIO !== 0 && {
aspectRatio: GRID_ASPECT_RATIO,
},
}}
>
<PhotoMedium
className={clsx(
'flex w-full h-full',
// Prevent photo navigation when selecting
isSelectingPhotos && 'pointer-events-none',
classNamePhoto,
)}
{...{
photo,
...categories,
selected: photo.id === selectedPhoto?.id,
priority: prioritizeInitialPhotos ? index < 6 : undefined,
onVisible: index === photos.length - 1
? onLastPhotoVisible
: undefined,
}}
/>
{isSelectingPhotos &&
<SelectTileOverlay
isSelected={isSelected}
onSelectChange={() => setSelectedPhotoIds?.(isSelected
? (selectedPhotoIds ?? []).filter(id => id !== photo.id)
: (selectedPhotoIds ?? []).concat(photo.id),
)}
/>}
</div>;
}).concat(additionalTile ? <>{additionalTile}</> : [])}
itemKeys={photos.map(photo => photo.id)
.concat(additionalTile ? ['more'] : [])}
/>
</div>
);
};