feat: Enable drag-and-drop reordering in ModerationGroupImagesPage

- Added PUT /api/admin/groups/:groupId/reorder endpoint
- Implemented handleReorder in ModerationGroupImagesPage
- Uses adminRequest API with proper error handling
- Same mobile touch support as ManagementPortalPage
This commit is contained in:
Matthias Lotz 2025-11-27 20:09:08 +01:00
parent 215acaa67f
commit 91d6d06687
5 changed files with 257 additions and 7 deletions

View File

@ -2573,6 +2573,96 @@
}
}
},
"/api/admin/groups/{groupId}/reorder": {
"put": {
"tags": [
"Admin - Groups Moderation"
],
"summary": "Reorder images in a group",
"description": "Updates the display order of images within a group",
"parameters": [
{
"name": "groupId",
"in": "path",
"required": true,
"type": "string",
"description": "Group ID",
"example": "abc123def456"
}
],
"responses": {
"200": {
"description": "Images reordered successfully",
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"message": {
"type": "string",
"example": "Image order updated successfully"
},
"data": {
"type": "object",
"properties": {
"updatedImages": {
"type": "number",
"example": 5
}
}
}
},
"xml": {
"name": "main"
}
}
},
"400": {
"description": "Invalid imageIds parameter"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Group not found"
},
"500": {
"description": "Internal Server Error"
}
},
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"imageIds"
],
"properties": {
"imageIds": {
"type": "array",
"items": {
"type": "integer"
},
"example": [
5,
3,
1,
2,
4
],
"description": "Array of image IDs in new order"
}
}
}
}
}
}
}
},
"/api/admin/{groupId}/reorder": {
"put": {
"tags": [

View File

@ -978,6 +978,120 @@ router.patch('/groups/:groupId/images/:imageId', async (req, res) => {
}
});
router.put('/groups/:groupId/reorder', async (req, res) => {
/*
#swagger.tags = ['Admin - Groups Moderation']
#swagger.summary = 'Reorder images in a group'
#swagger.description = 'Updates the display order of images within a group'
#swagger.parameters['groupId'] = {
in: 'path',
required: true,
type: 'string',
description: 'Group ID',
example: 'abc123def456'
}
#swagger.requestBody = {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['imageIds'],
properties: {
imageIds: {
type: 'array',
items: { type: 'integer' },
example: [5, 3, 1, 2, 4],
description: 'Array of image IDs in new order'
}
}
}
}
}
}
#swagger.responses[200] = {
description: 'Images reordered successfully',
schema: {
success: true,
message: 'Image order updated successfully',
data: {
updatedImages: 5
}
}
}
#swagger.responses[400] = {
description: 'Invalid imageIds parameter'
}
#swagger.responses[404] = {
description: 'Group not found'
}
*/
try {
const { groupId } = req.params;
const { imageIds } = req.body;
// Validate imageIds
if (!imageIds || !Array.isArray(imageIds) || imageIds.length === 0) {
return res.status(400).json({
success: false,
error: 'imageIds array is required and cannot be empty'
});
}
// Validate that all imageIds are numbers
const invalidIds = imageIds.filter(id => !Number.isInteger(id) || id <= 0);
if (invalidIds.length > 0) {
return res.status(400).json({
success: false,
error: `Invalid image IDs: ${invalidIds.join(', ')}. Image IDs must be positive integers`
});
}
// Verify group exists
const groupData = await GroupRepository.getGroupById(groupId);
if (!groupData) {
return res.status(404).json({
success: false,
error: 'Group not found',
message: `Gruppe mit ID ${groupId} wurde nicht gefunden`
});
}
// Execute reorder using GroupRepository
const result = await GroupRepository.updateImageOrder(groupId, imageIds);
res.status(200).json({
success: true,
message: 'Image order updated successfully',
data: result
});
} catch (error) {
console.error(`[ADMIN] Error reordering images for group ${req.params.groupId}:`, error.message);
// Handle specific errors
if (error.message.includes('not found')) {
return res.status(404).json({
success: false,
error: 'Group or images not found'
});
}
if (error.message.includes('mismatch')) {
return res.status(400).json({
success: false,
error: error.message
});
}
res.status(500).json({
success: false,
error: 'Failed to reorder images',
message: 'Fehler beim Sortieren der Bilder'
});
}
});
router.delete('/groups/:groupId', async (req, res) => {
/*
#swagger.tags = ['Admin - Groups Moderation']

View File

@ -239,13 +239,18 @@
background: rgba(0,0,0,0.7);
color: white;
border-radius: 4px;
padding: 4px 8px;
font-size: 14px;
padding: 8px 12px;
font-size: 16px;
cursor: grab;
user-select: none;
z-index: 10;
opacity: 0;
transition: opacity 0.2s;
opacity: 1; /* Always visible on mobile */
transition: opacity 0.2s, background 0.2s;
touch-action: none; /* Prevent scrolling when touching handle */
}
.drag-handle:hover {
background: rgba(0,0,0,0.9);
}
.image-gallery-card.reorderable:hover .drag-handle {

View File

@ -5,6 +5,7 @@ import {
closestCenter,
KeyboardSensor,
PointerSensor,
TouchSensor,
useSensor,
useSensors
} from '@dnd-kit/core';
@ -34,11 +35,17 @@ const ImageGallery = ({
imageDescriptions = {},
onDescriptionChange = null
}) => {
// Sensors for drag and drop (touch-friendly)
// Sensors for drag and drop (desktop + mobile optimized)
const sensors = useSensors(
useSensor(TouchSensor, {
activationConstraint: {
delay: 0, // No delay - allow immediate dragging
tolerance: 0, // No tolerance - precise control
},
}),
useSensor(PointerSensor, {
activationConstraint: {
distance: 8, // Require 8px movement before drag starts
distance: 5, // Require 5px movement before drag starts (desktop)
},
}),
useSensor(KeyboardSensor, {

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
// Services
import { adminGet } from '../../services/adminApi';
import { adminGet, adminRequest } from '../../services/adminApi';
import { handleAdminError } from '../../services/adminErrorHandler';
import AdminSessionGate from '../AdminAuth/AdminSessionGate.jsx';
import { useAdminSession } from '../../contexts/AdminSessionContext.jsx';
@ -14,6 +14,9 @@ import ImageDescriptionManager from '../ComponentUtils/ImageDescriptionManager';
import GroupMetadataEditor from '../ComponentUtils/GroupMetadataEditor';
import Loading from '../ComponentUtils/LoadingAnimation/Loading';
// UI
import Swal from 'sweetalert2';
/**
* ModerationGroupImagesPage - Admin page for moderating group images
*
@ -71,6 +74,35 @@ const ModerationGroupImagesPage = () => {
loadGroup();
}, [isAuthenticated, loadGroup]);
const handleReorder = async (newOrder) => {
if (!group || !groupId) {
console.error('No groupId available for reordering');
return;
}
try {
const imageIds = newOrder.map(img => img.id);
// Use admin API
await adminRequest(`/api/admin/groups/${groupId}/reorder`, 'PUT', {
imageIds: imageIds
});
await Swal.fire({
icon: 'success',
title: 'Gespeichert',
text: 'Die neue Reihenfolge wurde gespeichert.',
timer: 1500,
showConfirmButton: false
});
await loadGroup();
} catch (error) {
console.error('Error reordering images:', error);
await handleAdminError(error, 'Reihenfolge speichern');
}
};
const renderContent = () => {
if (loading) return <Loading />;
if (error) return <div className="moderation-error">{error}</div>;
@ -87,6 +119,8 @@ const ModerationGroupImagesPage = () => {
groupId={groupId}
onRefresh={loadGroup}
mode="moderate"
enableReordering={true}
onReorder={handleReorder}
/>
{/* Group Metadata Editor */}