- Refactored ManagementPortalPage, MultiUploadPage, ModerationGroupImagesPage - Created reusable modular components with mode support: * ImageDescriptionManager (manage/moderate modes) * GroupMetadataEditor (edit/upload/moderate modes) * ConsentManager (edit/upload modes) - Replaced Material-UI Buttons with HTML buttons + CSS classes - Fixed image descriptions upload (preview ID to filename mapping) - Reduced ModerationGroupImagesPage from 281 to 107 lines - Updated ModerationGroupsPage and GroupsOverviewPage button styles - All pages now use consistent Paper boxes with headings - Inline Material-UI Alerts instead of SweetAlert2 popups (except destructive actions) - Icons: 💾 save, ↩ discard, 🗑️ delete consistently used
187 lines
5.1 KiB
JavaScript
187 lines
5.1 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);
|
|
|
|
// 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}
|
|
/>
|
|
|
|
{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;
|