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 ; if (error) return
{error}
; @@ -87,6 +119,8 @@ const ModerationGroupImagesPage = () => { groupId={groupId} onRefresh={loadGroup} mode="moderate" + enableReordering={true} + onReorder={handleReorder} /> {/* Group Metadata Editor */}