Project-Image-Uploader/frontend/src/Components/ComponentUtils/ImageGalleryCard.js
matthias.lotz 7564525c7e feat: implement drag-and-drop reordering infrastructure
Phase 1 (Backend API):
 GroupRepository.updateImageOrder() with SQL transactions
 PUT /api/groups/:groupId/reorder API route with validation
 Manual testing: Reordering verified working (group qion_-lT1)
 Error handling: Invalid IDs, missing groups, empty arrays

Phase 2 (Frontend DnD):
 @dnd-kit/core packages installed
 ReorderService.js for API communication
 useReordering.js custom hook with optimistic updates
 ImageGalleryCard.js extended with drag handles & sortable
 ImageGallery.js with DndContext and SortableContext
 CSS styles for drag states, handles, touch-friendly mobile

Next: Integration with ModerationGroupImagesPage
2025-11-03 21:06:39 +01:00

245 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import PropTypes from 'prop-types';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import './Css/ImageGallery.css';
import { getImageSrc, getGroupPreviewSrc } from '../../Utils/imageUtils';
const ImageGalleryCard = ({
item,
onApprove,
onViewImages,
onDelete,
isPending,
showActions = true,
index,
mode = 'group', // 'group', 'moderation', or 'preview'
hidePreview = false, // Hide the preview image section
enableReordering = false, // Enable drag-and-drop reordering
isDragOverlay = false // Special styling when used as drag overlay
}) => {
// Handle both group data and individual image preview data
let previewUrl = null;
let title = '';
let subtitle = '';
let description = '';
let uploadDate = '';
let imageCount = 0;
let itemId = '';
if (mode === 'preview' || mode === 'single-image') {
// Preview mode: display individual images
// Use preview image (optimized thumbnails for gallery)
previewUrl = getImageSrc(item, true);
title = item.originalName || item.name || 'Bild';
// Show capture date if available
if (item.captureDate) {
subtitle = `Aufnahme: ${new Date(item.captureDate).toLocaleDateString('de-DE')}`;
}
itemId = item.id;
} else {
// Group mode: display group information
const group = item;
// Use preview image from first image in group
previewUrl = getGroupPreviewSrc(group, true);
title = group.title;
// Only show name if it exists and is not empty/null/undefined
subtitle = (group.name && group.name !== 'null') ? `${group.year}${group.name}` : `${group.year}`;
description = group.description;
uploadDate = group.uploadDate;
imageCount = group.imageCount;
itemId = group.groupId;
}
// Drag-and-Drop setup (only for reorderable items)
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({
id: itemId,
disabled: !enableReordering
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
cursor: enableReordering ? 'grab' : 'default'
};
// CSS classes for different states
const cardClasses = [
'image-gallery-card',
isPending ? 'pending' : 'approved',
'card-stretch',
enableReordering ? 'reorderable' : '',
isDragging ? 'dragging' : '',
isDragOverlay ? 'drag-overlay' : ''
].filter(Boolean).join(' ');
return (
<div
ref={setNodeRef}
style={style}
className={cardClasses}
{...attributes}
{...(enableReordering ? listeners : {})}
>
{!hidePreview && (
<div className="image-gallery-card-preview">
{previewUrl ? (
<img src={previewUrl} alt="Preview" className="image-gallery-card-preview-image" />
) : (
<div className="image-gallery-card-no-preview">Kein Vorschaubild</div>
)}
{/* Drag Handle - nur sichtbar wenn Reordering aktiviert */}
{enableReordering && (
<div className="drag-handle" title="Bild verschieben">
<span></span>
</div>
)}
{mode === 'preview' && index !== undefined && (
<div className="image-gallery-card-image-order">{index + 1}</div>
)}
{mode !== 'preview' && imageCount > 0 && (
<div className="image-gallery-card-image-count">{imageCount} Bilder</div>
)}
</div>
)}
<div className="image-gallery-card-info">
<h3>{title}</h3>
{subtitle && <p className="image-gallery-card-meta">{subtitle}</p>}
{description && (
<p className="image-gallery-card-description">{description}</p>
)}
{uploadDate && (
<p className="image-gallery-card-upload-date">
Hochgeladen: {new Date(uploadDate).toLocaleDateString('de-DE')}
</p>
)}
{/* Additional metadata for preview mode */}
{mode === 'preview' && item.remoteUrl && item.remoteUrl.includes('/download/') && (
<div className="image-gallery-card-file-meta">
Server-Datei: {item.remoteUrl.split('/').pop()}
</div>
)}
{mode === 'preview' && item.filePath && !item.remoteUrl && (
<div className="image-gallery-card-file-meta">
Server-Datei: {item.filePath.split('/').pop()}
</div>
)}
</div>
{/* Only show actions section if there are actions to display */}
{(showActions || (mode !== 'single-image' && !showActions)) && (
<div className="image-gallery-card-actions">
{showActions ? (
mode === 'preview' ? (
// Preview mode actions (for upload preview)
<>
<button
className="btn btn-danger"
onClick={() => onDelete(index !== undefined ? index : itemId)}
>
🗑 Löschen
</button>
<button
className="btn btn-secondary btn-sm"
disabled
>
Sort
</button>
</>
) : (
// Moderation mode actions (for existing groups)
<>
<button
className="btn btn-secondary"
onClick={() => onViewImages(item)}
>
Gruppe editieren
</button>
{isPending ? (
<button
className="btn btn-success"
onClick={() => onApprove(itemId, true)}
>
Freigeben
</button>
) : (
<button
className="btn btn-warning"
onClick={() => onApprove(itemId, false)}
>
Sperren
</button>
)}
<button
className="btn btn-danger"
onClick={() => onDelete(itemId)}
>
🗑 Löschen
</button>
</>
)
) : mode !== 'single-image' ? (
// Public view mode (only for group cards, not single images)
<button
className="view-button"
onClick={() => onViewImages(item)}
title="Anzeigen"
>
Anzeigen
</button>
) : null}
</div>
)}
</div>
);
};
ImageGalleryCard.propTypes = {
item: PropTypes.object.isRequired,
onApprove: PropTypes.func,
onViewImages: PropTypes.func,
onDelete: PropTypes.func,
isPending: PropTypes.bool,
showActions: PropTypes.bool,
index: PropTypes.number,
mode: PropTypes.oneOf(['group', 'moderation', 'preview', 'single-image']),
hidePreview: PropTypes.bool,
enableReordering: PropTypes.bool,
isDragOverlay: PropTypes.bool
};
ImageGalleryCard.defaultProps = {
onApprove: () => {},
onViewImages: () => {},
onDelete: () => {},
isPending: false,
showActions: true,
mode: 'group',
hidePreview: false,
enableReordering: false,
isDragOverlay: false
};
export default ImageGalleryCard;