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.
241 lines
6.5 KiB
JavaScript
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;
|