Project-Image-Uploader/frontend/src/Components/ComponentUtils/ImageDescriptionManager.js
matthias.lotz 89e35e7de6 fix: Use correct image ID when deleting images in preview mode
Changed ImageGalleryCard to pass itemId (image.id) instead of index
when deleting images in preview mode. This fixes 'Image not found' error
when attempting to delete individual images in ManagementPortalPage
and ModerationGroupImagesPage.

The index was being passed to the API, but the API expects the actual
database image ID.
2025-11-15 18:59:21 +01:00

241 lines
6.5 KiB
JavaScript

import React, { useState } from 'react';
import { Box, Typography, Paper } from '@mui/material';
import Swal from 'sweetalert2';
import ImageGallery from './ImageGallery';
/**
* Manages image descriptions with save functionality
* Wraps ImageGallery and provides batch save for all descriptions
*
* @param mode - 'manage' (uses token) or 'moderate' (uses groupId)
*/
function ImageDescriptionManager({
images,
token,
groupId,
enableReordering = false,
onReorder,
onRefresh,
mode = 'manage'
}) {
const [imageDescriptions, setImageDescriptions] = useState({});
const [originalDescriptions, setOriginalDescriptions] = useState({});
const [isEditMode, setIsEditMode] = useState(false);
const [saving, setSaving] = useState(false);
// Handle deleting a single image
const handleDeleteImage = async (imageId) => {
const result = await Swal.fire({
title: 'Bild löschen?',
text: 'Möchten Sie dieses Bild wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ja, löschen',
cancelButtonText: 'Abbrechen'
});
if (!result.isConfirmed) return;
try {
// Different API endpoints for manage vs moderate
const endpoint = mode === 'moderate'
? `/groups/${groupId}/images/${imageId}`
: `/api/manage/${token}/images/${imageId}`;
const res = await fetch(endpoint, {
method: 'DELETE'
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || 'Fehler beim Löschen des Bildes');
}
await Swal.fire({
icon: 'success',
title: 'Gelöscht',
text: 'Bild wurde erfolgreich gelöscht.',
timer: 2000,
showConfirmButton: false
});
// Refresh data
if (onRefresh) {
await onRefresh();
}
} catch (error) {
console.error('Error deleting image:', error);
Swal.fire({
icon: 'error',
title: 'Fehler',
text: error.message || 'Bild konnte nicht gelöscht werden'
});
}
};
// Initialize descriptions from images
React.useEffect(() => {
if (images && images.length > 0) {
const descriptions = {};
images.forEach(img => {
descriptions[img.id] = img.imageDescription || '';
});
setImageDescriptions(descriptions);
setOriginalDescriptions(descriptions);
}
}, [images]);
const handleDescriptionChange = (imageId, description) => {
setImageDescriptions(prev => ({
...prev,
[imageId]: description
}));
};
const hasChanges = () => {
return JSON.stringify(imageDescriptions) !== JSON.stringify(originalDescriptions);
};
const handleSave = async () => {
if (!hasChanges()) {
Swal.fire({
icon: 'info',
title: 'Keine Änderungen',
text: 'Es wurden keine Änderungen an den Beschreibungen vorgenommen.'
});
return;
}
try {
setSaving(true);
// Build descriptions array for API
const descriptions = Object.entries(imageDescriptions).map(([imageId, description]) => ({
imageId: parseInt(imageId),
description: description || null
}));
// Different API endpoints for manage vs moderate
const endpoint = mode === 'moderate'
? `/groups/${groupId}/images/batch-description`
: `/api/manage/${token}/images/descriptions`;
const method = mode === 'moderate' ? 'PATCH' : 'PUT';
const res = await fetch(endpoint, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ descriptions })
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || 'Fehler beim Speichern der Beschreibungen');
}
await Swal.fire({
icon: 'success',
title: 'Gespeichert',
text: 'Bildbeschreibungen wurden erfolgreich aktualisiert.',
timer: 2000,
showConfirmButton: false
});
// Update original descriptions
setOriginalDescriptions(imageDescriptions);
// Refresh data if callback provided
if (onRefresh) {
await onRefresh();
}
} catch (error) {
console.error('Error saving descriptions:', error);
Swal.fire({
icon: 'error',
title: 'Fehler',
text: error.message || 'Beschreibungen konnten nicht gespeichert werden'
});
} finally {
setSaving(false);
}
};
const handleDiscard = () => {
setImageDescriptions(originalDescriptions);
setIsEditMode(false);
};
const handleEditToggle = () => {
if (isEditMode && hasChanges()) {
// Warn user if trying to leave edit mode with unsaved changes
Swal.fire({
icon: 'warning',
title: 'Ungespeicherte Änderungen',
text: 'Du hast ungespeicherte Änderungen. Bitte speichere oder verwerfe sie zuerst.',
confirmButtonText: 'OK'
});
return; // Don't toggle edit mode
}
if (isEditMode) {
// Discard changes when leaving edit mode without saving
setImageDescriptions({ ...originalDescriptions });
}
setIsEditMode(!isEditMode);
};
return (
<Paper
sx={{
p: 3,
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
border: '2px solid #e0e0e0'
}}
>
{/* Component Header */}
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600, mb: 2 }}>
Bildbeschreibungen
</Typography>
<ImageGallery
items={images}
mode="preview"
isEditMode={isEditMode}
onEditMode={handleEditToggle}
enableReordering={enableReordering}
onReorder={onReorder}
imageDescriptions={imageDescriptions}
onDescriptionChange={handleDescriptionChange}
onDelete={handleDeleteImage}
/>
{hasChanges() && (
<Box sx={{ mt: 2, display: 'flex', gap: 1, justifyContent: 'center' }}>
<button
className="btn btn-success"
onClick={handleSave}
disabled={saving}
>
{saving ? '⏳ Speichern...' : '💾 Beschreibungen speichern'}
</button>
<button
className="btn btn-secondary"
onClick={handleDiscard}
disabled={saving}
>
Verwerfen
</button>
</Box>
)}
</Paper>
);
}
export default ImageDescriptionManager;