From 9fda54bd06d37cd8f8f71056bf4f59e158cd8112 Mon Sep 17 00:00:00 2001 From: paul Date: Sun, 14 Sep 2025 17:02:03 +0200 Subject: [PATCH] feat(select): add per-tile checkbox selection in Admin grid and all gallery layouts; tile click opens viewer; checkbox toggles selection; auto-enable selection mode; add testids --- .../src/components/admin/AdminPhotoGrid.tsx | 46 ++++++----- .../gallery/PhotoGridWithLayouts.tsx | 10 ++- .../gallery/layouts/GridGalleryLayout.tsx | 34 ++++---- .../gallery/layouts/HeroGalleryLayout.tsx | 31 ++++---- .../gallery/layouts/MasonryGalleryLayout.tsx | 34 ++++---- .../gallery/layouts/MosaicGalleryLayout.tsx | 78 ++++++++++++------- .../gallery/layouts/TimelineGalleryLayout.tsx | 31 ++++---- 7 files changed, 159 insertions(+), 105 deletions(-) diff --git a/frontend/src/components/admin/AdminPhotoGrid.tsx b/frontend/src/components/admin/AdminPhotoGrid.tsx index 0831661..2714b70 100644 --- a/frontend/src/components/admin/AdminPhotoGrid.tsx +++ b/frontend/src/components/admin/AdminPhotoGrid.tsx @@ -29,7 +29,10 @@ export const AdminPhotoGrid: React.FC = ({ if (e) { e.stopPropagation(); } - + // Auto-enable selection mode when selecting via checkbox + if (!isSelectionMode) { + setIsSelectionMode(true); + } const newSelected = new Set(selectedPhotos); if (newSelected.has(photoId)) { newSelected.delete(photoId); @@ -126,7 +129,7 @@ export const AdminPhotoGrid: React.FC = ({ {isSelectionMode ? 'Cancel Selection' : 'Select Photos'} - {isSelectionMode && ( + {(isSelectionMode || selectedPhotos.size > 0) && ( <> {/* Thumbnail */}
@@ -240,7 +250,7 @@ export const AdminPhotoGrid: React.FC = ({ {/* Category Badge */} {photo.category_name && ( -
+
{photo.category_name} @@ -276,4 +286,4 @@ export const AdminPhotoGrid: React.FC = ({ )}
); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/gallery/PhotoGridWithLayouts.tsx b/frontend/src/components/gallery/PhotoGridWithLayouts.tsx index 37e1564..cbf9558 100644 --- a/frontend/src/components/gallery/PhotoGridWithLayouts.tsx +++ b/frontend/src/components/gallery/PhotoGridWithLayouts.tsx @@ -81,6 +81,14 @@ export const PhotoGridWithLayouts: React.FC = ({ }; const handlePhotoSelect = (photoId: number) => { + // Auto-enable selection mode when selecting via checkbox + if (!isSelectionMode) { + if (parentToggleSelectionMode) { + parentToggleSelectionMode(); + } else { + setLocalSelectionMode(true); + } + } const newSelected = new Set(selectedPhotos); if (newSelected.has(photoId)) { newSelected.delete(photoId); @@ -279,4 +287,4 @@ export const PhotoGridWithLayouts: React.FC = ({ )} ); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx b/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx index 217dd99..ea06553 100644 --- a/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx @@ -12,6 +12,7 @@ interface GridPhotoProps { isSelectionMode: boolean; onClick: (e: React.MouseEvent) => void; onDownload: (e: React.MouseEvent) => void; + onToggleSelect: () => void; animationType?: string; allowDownloads?: boolean; slug?: string; @@ -26,6 +27,7 @@ const GridPhoto: React.FC = ({ isSelectionMode, onClick, onDownload, + onToggleSelect, animationType = 'fade', allowDownloads = true, slug, @@ -105,13 +107,22 @@ const GridPhoto: React.FC = ({ )}
- {isSelectionMode && ( -
-
- {isSelected && } -
+ {/* Selection Checkbox (visible on hover or when selected) */} + {/* Feedback Indicators */} {feedbackEnabled && (photo.comment_count > 0 || photo.average_rating > 0 || photo.like_count > 0) && ( @@ -187,13 +198,8 @@ export const GridGalleryLayout: React.FC = ({ photo={photo} isSelected={selectedPhotos.has(photo.id)} isSelectionMode={isSelectionMode} - onClick={() => { - if (isSelectionMode && onPhotoSelect) { - onPhotoSelect(photo.id); - } else { - onPhotoClick(index); - } - }} + onClick={() => onPhotoClick(index)} + onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo.id)} onDownload={(e) => onDownload(photo, e)} animationType={animation} allowDownloads={allowDownloads} @@ -205,4 +211,4 @@ export const GridGalleryLayout: React.FC = ({ ))}
); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx b/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx index d22e970..c4e0c54 100644 --- a/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx @@ -152,13 +152,7 @@ export const HeroGalleryLayout: React.FC = ({
{ - if (isSelectionMode && onPhotoSelect) { - onPhotoSelect(photo.id); - } else { - onPhotoClick(actualIndex); - } - }} + onClick={() => onPhotoClick(actualIndex)} > = ({ )}
- {isSelectionMode && ( -
-
- {selectedPhotos.has(photo.id) && } -
+ {/* Selection Checkbox (visible on hover or when selected) */} +
); })}
); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx b/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx index be7e3e2..6741910 100644 --- a/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx @@ -11,6 +11,7 @@ interface MasonryPhotoProps { isSelectionMode: boolean; onClick: (e: React.MouseEvent) => void; onDownload: (e: React.MouseEvent) => void; + onToggleSelect: () => void; style?: React.CSSProperties; allowDownloads?: boolean; feedbackEnabled?: boolean; @@ -22,6 +23,7 @@ const MasonryPhoto: React.FC = ({ isSelectionMode, onClick, onDownload, + onToggleSelect, style, allowDownloads = true, feedbackEnabled = false @@ -104,13 +106,22 @@ const MasonryPhoto: React.FC = ({ )} - {isSelectionMode && ( -
-
- {isSelected && } -
+ {/* Selection Checkbox (visible on hover or when selected) */} + {photo.type === 'collage' && (
@@ -182,14 +193,9 @@ export const MasonryGalleryLayout: React.FC = ({ photo={photo} isSelected={selectedPhotos.has(photo.id)} isSelectionMode={isSelectionMode} - onClick={() => { - if (isSelectionMode && onPhotoSelect) { - onPhotoSelect(photo.id); - } else { - onPhotoClick(originalIndex); - } - }} + onClick={() => onPhotoClick(originalIndex)} onDownload={(e) => onDownload(photo, e)} + onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo.id)} allowDownloads={allowDownloads} feedbackEnabled={feedbackEnabled} /> @@ -199,4 +205,4 @@ export const MasonryGalleryLayout: React.FC = ({ ))}
); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx b/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx index 1bd672f..40de30a 100644 --- a/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx @@ -11,6 +11,7 @@ interface MosaicPhotoProps { isSelectionMode: boolean; onClick: (e: React.MouseEvent) => void; onDownload: (e: React.MouseEvent) => void; + onToggleSelect: () => void; className?: string; allowDownloads?: boolean; } @@ -21,6 +22,7 @@ const MosaicPhoto: React.FC = ({ isSelectionMode, onClick, onDownload, + onToggleSelect, className = '', allowDownloads = true }) => { @@ -69,13 +71,22 @@ const MosaicPhoto: React.FC = ({ )}
- {isSelectionMode && ( -
-
- {isSelected && } -
+ {/* Selection Checkbox (visible on hover or when selected) */} + {photo.type === 'collage' && (
@@ -136,8 +147,9 @@ export const MosaicGalleryLayout: React.FC = ({ photo={photo0} isSelected={selectedPhotos.has(photo0.id)} isSelectionMode={isSelectionMode} - onClick={() => handlePhotoClick(idx0, photo0.id)} + onClick={() => onPhotoClick(idx0)} onDownload={(e) => onDownload(photo0, e)} + onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo0.id)} className="col-span-1" allowDownloads={allowDownloads} /> @@ -148,22 +160,24 @@ export const MosaicGalleryLayout: React.FC = ({ photo={photo1} isSelected={selectedPhotos.has(photo1.id)} isSelectionMode={isSelectionMode} - onClick={() => handlePhotoClick(idx1, photo1.id)} - onDownload={(e) => onDownload(photo1, e)} - className="" - allowDownloads={allowDownloads} - /> + onClick={() => onPhotoClick(idx1)} + onDownload={(e) => onDownload(photo1, e)} + onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo1.id)} + className="" + allowDownloads={allowDownloads} + /> )} {photo2 && ( handlePhotoClick(idx2, photo2.id)} - onDownload={(e) => onDownload(photo2, e)} - className="" - allowDownloads={allowDownloads} - /> + onClick={() => onPhotoClick(idx2)} + onDownload={(e) => onDownload(photo2, e)} + onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo2.id)} + className="" + allowDownloads={allowDownloads} + /> )}
@@ -209,8 +223,9 @@ export const MosaicGalleryLayout: React.FC = ({ photo={photo0} isSelected={selectedPhotos.has(photo0.id)} isSelectionMode={isSelectionMode} - onClick={() => handlePhotoClick(idx0, photo0.id)} + onClick={() => onPhotoClick(idx0)} onDownload={(e) => onDownload(photo0, e)} + onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo0.id)} className="col-span-2" allowDownloads={allowDownloads} /> @@ -221,22 +236,24 @@ export const MosaicGalleryLayout: React.FC = ({ photo={photo1} isSelected={selectedPhotos.has(photo1.id)} isSelectionMode={isSelectionMode} - onClick={() => handlePhotoClick(idx1, photo1.id)} - onDownload={(e) => onDownload(photo1, e)} - className="" - allowDownloads={allowDownloads} - /> + onClick={() => onPhotoClick(idx1)} + onDownload={(e) => onDownload(photo1, e)} + onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo1.id)} + className="" + allowDownloads={allowDownloads} + /> )} {photo2 && ( handlePhotoClick(idx2, photo2.id)} - onDownload={(e) => onDownload(photo2, e)} - className="" - allowDownloads={allowDownloads} - /> + onClick={() => onPhotoClick(idx2)} + onDownload={(e) => onDownload(photo2, e)} + onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo2.id)} + className="" + allowDownloads={allowDownloads} + /> )} @@ -263,8 +280,9 @@ export const MosaicGalleryLayout: React.FC = ({ photo={photo} isSelected={selectedPhotos.has(photo.id)} isSelectionMode={isSelectionMode} - onClick={() => handlePhotoClick(index, photo.id)} + onClick={() => onPhotoClick(index)} onDownload={(e) => onDownload(photo, e)} + onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo.id)} className="aspect-square" allowDownloads={allowDownloads} /> @@ -282,4 +300,4 @@ export const MosaicGalleryLayout: React.FC = ({ {renderMosaicLayout()} ); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx b/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx index c34cb4b..098a204 100644 --- a/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx @@ -90,13 +90,7 @@ export const TimelineGalleryLayout: React.FC = ({
{ - if (isSelectionMode && onPhotoSelect) { - onPhotoSelect(photo.id); - } else { - onPhotoClick(actualIndex); - } - }} + onClick={() => onPhotoClick(actualIndex)} > = ({ )}
- {isSelectionMode && ( -
-
- {selectedPhotos.has(photo.id) && } -
+ {/* Selection Checkbox (visible on hover or when selected) */} +
); })} @@ -157,4 +160,4 @@ export const TimelineGalleryLayout: React.FC = ({ ); -}; \ No newline at end of file +};