{wrapMoreButtonInGrid
?
diff --git a/src/photo/PhotoGrid.tsx b/src/photo/PhotoGrid.tsx
index 8a882e4c..7923abc1 100644
--- a/src/photo/PhotoGrid.tsx
+++ b/src/photo/PhotoGrid.tsx
@@ -5,13 +5,17 @@ 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 {
+ GRID_ASPECT_RATIO,
+ MASONRY_GRID_ENABLED,
+} 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';
+import PhotoGridMasonry from './PhotoGridMasonry';
export default function PhotoGrid({
photos,
@@ -54,6 +58,76 @@ export default function PhotoGrid({
togglePhotoSelection,
} = useSelectPhotosState();
+ const photoNodes = photos.map((photo, index) => {
+ const isSelected = (
+ selectedPhotoIds?.includes(photo.id) ||
+ isSelectingAllPhotos
+ ) ?? false;
+ return
+
+ {isSelectingPhotos &&
+
togglePhotoSelection?.(photo.id)}
+ />}
+ ;
+ });
+
+ const allItems = photoNodes.concat(
+ additionalTile ? [
{additionalTile}
] : [],
+ );
+
+ if (MASONRY_GRID_ENABLED) {
+ return (
+
+ );
+ }
+
return (
{
- const isSelected = (
- selectedPhotoIds?.includes(photo.id) ||
- isSelectingAllPhotos
- ) ?? false;
- return
-
- {isSelectingPhotos &&
-
togglePhotoSelection?.(photo.id)}
- />}
- ;
- }).concat(additionalTile ? <>{additionalTile}> : [])}
+ items={allItems}
itemKeys={photos.map(photo => photo.id)
.concat(additionalTile ? ['more'] : [])}
/>
diff --git a/src/photo/PhotoGridContainer.tsx b/src/photo/PhotoGridContainer.tsx
index ce3be8c5..f3689d2a 100644
--- a/src/photo/PhotoGridContainer.tsx
+++ b/src/photo/PhotoGridContainer.tsx
@@ -8,6 +8,7 @@ import AnimateItems from '@/components/AnimateItems';
import { ComponentProps, useCallback, useState, ReactNode } from 'react';
import { GRID_SPACE_CLASSNAME } from '@/components';
import { SortBy } from './sort';
+import { MASONRY_GRID_ENABLED } from '@/app/config';
export default function PhotoGridContainer({
cacheKey,
@@ -31,6 +32,9 @@ export default function PhotoGridContainer({
sidebar?: ReactNode
className?: string
} & ComponentProps
) {
+ const shouldRenderInitialGrid =
+ !MASONRY_GRID_ENABLED || count <= photos.length;
+
const [
shouldAnimateDynamicItems,
setShouldAnimateDynamicItems,
@@ -51,15 +55,18 @@ export default function PhotoGridContainer({
animateOnFirstLoadOnly
/>}
-
+ {shouldRenderInitialGrid && (
+
+ )}
{count > photos.length &&
, 'photos'>) {
return (
{
+ const handleResize = () => {
+ const width = window.innerWidth;
+ if (small) {
+ setColumns(width >= 480 ? 6 : 3);
+ } else if (isGridHighDensity) {
+ if (width >= 1024) setColumns(6);
+ else if (width >= 480) setColumns(4);
+ else setColumns(2);
+ } else {
+ if (width >= 1024) setColumns(4);
+ else if (width >= 768) setColumns(3);
+ else if (width >= 640) setColumns(4);
+ else setColumns(2);
+ }
+ };
+
+ handleResize();
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, [small, isGridHighDensity]);
+
+ return columns;
+}
+
+export default function PhotoGridMasonry({
+ photos,
+ photoNodes,
+ additionalTile,
+ small,
+ isGridHighDensity,
+ selectable,
+ className,
+ animate,
+ canStart,
+ animateOnFirstLoadOnly,
+ staggerOnFirstLoadOnly,
+ onAnimationCompleteAction,
+}: {
+ photos: Photo[];
+ photoNodes: ReactNode[];
+ additionalTile?: ReactNode;
+ small?: boolean;
+ isGridHighDensity?: boolean;
+ selectable?: boolean;
+ className?: string;
+ animate?: boolean;
+ canStart?: boolean;
+ animateOnFirstLoadOnly?: boolean;
+ staggerOnFirstLoadOnly?: boolean;
+ onAnimationCompleteAction?: () => void;
+}) {
+ const masonryColsCount = useMasonryColumns(small, isGridHighDensity);
+ const partitionedColumns = Array.from(
+ { length: masonryColsCount },
+ () => [] as ReactNode[],
+ );
+ const colHeights = new Array(masonryColsCount).fill(0);
+
+ photos.forEach((photo, index) => {
+ let shortestColIndex = 0;
+ let minHeight = colHeights[0];
+ for (let i = 1; i < masonryColsCount; i++) {
+ // Subtract tiny fraction to create tie breaker
+ // If columns equal in height, ensure photo's placed in left-most column
+ // (helps maintain left-to-right photo order)
+ if (colHeights[i] < minHeight - 0.0001) {
+ minHeight = colHeights[i];
+ shortestColIndex = i;
+ }
+ }
+ partitionedColumns[shortestColIndex].push(photoNodes[index]);
+ colHeights[shortestColIndex] += 1 / (photo.aspectRatio || 1);
+ });
+
+ if (additionalTile) {
+ let shortestColIndex = 0;
+ let minHeight = colHeights[0];
+ for (let i = 1; i < masonryColsCount; i++) {
+ if (colHeights[i] < minHeight - 0.0001) {
+ minHeight = colHeights[i];
+ shortestColIndex = i;
+ }
+ }
+ partitionedColumns[shortestColIndex].push(
+ {additionalTile}
,
+ );
+ colHeights[shortestColIndex] += 1;
+ }
+
+ return (
+
+
+ {partitionedColumns.map((colItems, i) => (
+
`col-${i}-item-${index}`)}
+ />
+ ))}
+
+
+ );
+}