diff --git a/backend/docs/openapi.json b/backend/docs/openapi.json
index 0d73c1f..c9e095a 100644
--- a/backend/docs/openapi.json
+++ b/backend/docs/openapi.json
@@ -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": [
diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js
index 7828336..6582a00 100644
--- a/backend/src/routes/admin.js
+++ b/backend/src/routes/admin.js
@@ -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']
diff --git a/frontend/src/Components/ComponentUtils/Css/ImageGallery.css b/frontend/src/Components/ComponentUtils/Css/ImageGallery.css
index 5b207a7..61afe3d 100644
--- a/frontend/src/Components/ComponentUtils/Css/ImageGallery.css
+++ b/frontend/src/Components/ComponentUtils/Css/ImageGallery.css
@@ -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 {
diff --git a/frontend/src/Components/ComponentUtils/ImageGallery.js b/frontend/src/Components/ComponentUtils/ImageGallery.js
index 15e2f96..e912508 100644
--- a/frontend/src/Components/ComponentUtils/ImageGallery.js
+++ b/frontend/src/Components/ComponentUtils/ImageGallery.js
@@ -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, {
diff --git a/frontend/src/Components/Pages/ModerationGroupImagesPage.js b/frontend/src/Components/Pages/ModerationGroupImagesPage.js
index 2c20c45..4d94f41 100644
--- a/frontend/src/Components/Pages/ModerationGroupImagesPage.js
+++ b/frontend/src/Components/Pages/ModerationGroupImagesPage.js
@@ -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