- Database: Add image_description column to images table - Repository: Add updateImageDescription & updateBatchImageDescriptions methods - API: Add PATCH endpoints for single and batch description updates - Upload: Support descriptions in batch upload - Frontend: ImageGalleryCard with Edit mode and textarea - Frontend: MultiUploadPage with description input - Frontend: ModerationGroupImagesPage with description editing - CSS: Styles for edit mode, textarea, and character counter Phase 1-4 complete: Backend + Core Frontend + Upload + Moderation
160 lines
4.5 KiB
JavaScript
160 lines
4.5 KiB
JavaScript
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import {
|
|
DndContext,
|
|
closestCenter,
|
|
KeyboardSensor,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors
|
|
} from '@dnd-kit/core';
|
|
import {
|
|
arrayMove,
|
|
SortableContext,
|
|
sortableKeyboardCoordinates,
|
|
rectSortingStrategy
|
|
} from '@dnd-kit/sortable';
|
|
import ImageGalleryCard from './ImageGalleryCard';
|
|
import './Css/ImageGallery.css';
|
|
|
|
const ImageGallery = ({
|
|
items,
|
|
onApprove,
|
|
onViewImages,
|
|
onDelete,
|
|
isPending,
|
|
showActions,
|
|
mode,
|
|
title,
|
|
emptyMessage = 'Keine Elemente vorhanden',
|
|
enableReordering = false,
|
|
onReorder = null,
|
|
isEditMode = false,
|
|
onEditMode = null,
|
|
imageDescriptions = {},
|
|
onDescriptionChange = null
|
|
}) => {
|
|
// Sensors for drag and drop (touch-friendly)
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
distance: 8, // Require 8px movement before drag starts
|
|
},
|
|
}),
|
|
useSensor(KeyboardSensor, {
|
|
coordinateGetter: sortableKeyboardCoordinates,
|
|
})
|
|
);
|
|
|
|
const handleDragEnd = (event) => {
|
|
const { active, over } = event;
|
|
|
|
if (active.id !== over?.id && onReorder) {
|
|
const oldIndex = items.findIndex(item =>
|
|
(item.id || item.groupId) === active.id
|
|
);
|
|
const newIndex = items.findIndex(item =>
|
|
(item.id || item.groupId) === over.id
|
|
);
|
|
|
|
if (oldIndex !== -1 && newIndex !== -1) {
|
|
const reorderedItems = arrayMove(items, oldIndex, newIndex);
|
|
onReorder(reorderedItems, oldIndex, newIndex);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (!items || items.length === 0) {
|
|
return (
|
|
<div className="image-gallery-empty">
|
|
<p>{emptyMessage}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const itemIds = items.map(item => item.id || item.groupId);
|
|
|
|
const galleryContent = (
|
|
<div className="image-gallery-grid">
|
|
{items.map((item, index) => (
|
|
<ImageGalleryCard
|
|
key={item.id || item.groupId || index}
|
|
item={item}
|
|
index={index}
|
|
onApprove={onApprove}
|
|
onViewImages={onViewImages}
|
|
onDelete={onDelete}
|
|
isPending={isPending}
|
|
showActions={showActions}
|
|
mode={mode}
|
|
enableReordering={enableReordering}
|
|
isEditMode={isEditMode}
|
|
onEditMode={onEditMode}
|
|
imageDescription={imageDescriptions[item.id || item.groupId] || ''}
|
|
onDescriptionChange={onDescriptionChange}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="image-gallery-container">
|
|
{title && (
|
|
<h2 className="image-gallery-title">{title}</h2>
|
|
)}
|
|
|
|
{enableReordering ? (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<SortableContext
|
|
items={itemIds}
|
|
strategy={rectSortingStrategy}
|
|
>
|
|
{galleryContent}
|
|
</SortableContext>
|
|
</DndContext>
|
|
) : (
|
|
galleryContent
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
ImageGallery.propTypes = {
|
|
items: PropTypes.array.isRequired,
|
|
onApprove: PropTypes.func,
|
|
onViewImages: PropTypes.func,
|
|
onDelete: PropTypes.func,
|
|
isPending: PropTypes.bool,
|
|
showActions: PropTypes.bool,
|
|
mode: PropTypes.oneOf(['group', 'moderation', 'preview', 'single-image']),
|
|
title: PropTypes.string,
|
|
emptyMessage: PropTypes.string,
|
|
enableReordering: PropTypes.bool,
|
|
onReorder: PropTypes.func,
|
|
isEditMode: PropTypes.bool,
|
|
onEditMode: PropTypes.func,
|
|
imageDescriptions: PropTypes.object,
|
|
onDescriptionChange: PropTypes.func
|
|
};
|
|
|
|
ImageGallery.defaultProps = {
|
|
onApprove: () => {},
|
|
onViewImages: () => {},
|
|
onDelete: () => {},
|
|
isPending: false,
|
|
showActions: true,
|
|
mode: 'group',
|
|
enableReordering: false,
|
|
onReorder: null,
|
|
isEditMode: false,
|
|
onEditMode: null,
|
|
imageDescriptions: {},
|
|
onDescriptionChange: null
|
|
};
|
|
|
|
export default ImageGallery;
|