refactor: Complete UI refactoring with modular components
- 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
This commit is contained in:
parent
4b9feec887
commit
bd7bdac000
|
|
@ -5,12 +5,17 @@ import ConsentCheckboxes from './MultiUpload/ConsentCheckboxes';
|
||||||
/**
|
/**
|
||||||
* Manages consents with save functionality
|
* Manages consents with save functionality
|
||||||
* Wraps ConsentCheckboxes and provides save for workshop + social media consents
|
* Wraps ConsentCheckboxes and provides save for workshop + social media consents
|
||||||
|
*
|
||||||
|
* @param mode - 'edit' (default) shows save/discard, 'upload' hides them
|
||||||
*/
|
*/
|
||||||
function ConsentManager({
|
function ConsentManager({
|
||||||
initialConsents,
|
initialConsents,
|
||||||
|
consents: externalConsents,
|
||||||
|
onConsentsChange,
|
||||||
token,
|
token,
|
||||||
groupId,
|
groupId,
|
||||||
onRefresh
|
onRefresh,
|
||||||
|
mode = 'edit'
|
||||||
}) {
|
}) {
|
||||||
// Initialize with proper defaults
|
// Initialize with proper defaults
|
||||||
const defaultConsents = {
|
const defaultConsents = {
|
||||||
|
|
@ -26,9 +31,14 @@ function ConsentManager({
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const [showEmailHint, setShowEmailHint] = useState(false);
|
const [showEmailHint, setShowEmailHint] = useState(false);
|
||||||
|
|
||||||
// Update ONLY ONCE when initialConsents first arrives
|
// In upload mode: use external state
|
||||||
|
const isUploadMode = mode === 'upload';
|
||||||
|
const currentConsents = isUploadMode ? externalConsents : consents;
|
||||||
|
const setCurrentConsents = isUploadMode ? onConsentsChange : setConsents;
|
||||||
|
|
||||||
|
// Update ONLY ONCE when initialConsents first arrives (edit mode only)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (initialConsents && !initialized) {
|
if (initialConsents && !initialized && !isUploadMode) {
|
||||||
// Deep copy to avoid shared references
|
// Deep copy to avoid shared references
|
||||||
const consentsCopy = {
|
const consentsCopy = {
|
||||||
workshopConsent: initialConsents.workshopConsent,
|
workshopConsent: initialConsents.workshopConsent,
|
||||||
|
|
@ -45,9 +55,10 @@ function ConsentManager({
|
||||||
|
|
||||||
setInitialized(true);
|
setInitialized(true);
|
||||||
}
|
}
|
||||||
}, [initialConsents, initialized]);
|
}, [initialConsents, initialized, isUploadMode]);
|
||||||
|
|
||||||
const hasChanges = () => {
|
const hasChanges = () => {
|
||||||
|
if (isUploadMode) return false; // No changes tracking in upload mode
|
||||||
// Check workshop consent
|
// Check workshop consent
|
||||||
if (consents.workshopConsent !== originalConsents.workshopConsent) {
|
if (consents.workshopConsent !== originalConsents.workshopConsent) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -191,14 +202,17 @@ function ConsentManager({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConsentCheckboxes
|
<ConsentCheckboxes
|
||||||
consents={consents}
|
consents={currentConsents}
|
||||||
onConsentChange={handleConsentChange}
|
onConsentChange={isUploadMode ? setCurrentConsents : handleConsentChange}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
mode="manage"
|
mode={isUploadMode ? "upload" : "manage"}
|
||||||
groupId={groupId}
|
groupId={groupId}
|
||||||
token={token}
|
token={token}
|
||||||
onSave={null}
|
onSave={null}
|
||||||
>
|
>
|
||||||
|
{/* Alerts and Buttons only in edit mode */}
|
||||||
|
{!isUploadMode && (
|
||||||
|
<>
|
||||||
{/* Success Message */}
|
{/* Success Message */}
|
||||||
{successMessage && (
|
{successMessage && (
|
||||||
<Alert severity="success" sx={{ mt: 3 }}>
|
<Alert severity="success" sx={{ mt: 3 }}>
|
||||||
|
|
@ -256,6 +270,8 @@ function ConsentManager({
|
||||||
</button>
|
</button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ConsentCheckboxes>
|
</ConsentCheckboxes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,17 @@ import DescriptionInput from './MultiUpload/DescriptionInput';
|
||||||
/**
|
/**
|
||||||
* Manages group metadata with save functionality
|
* Manages group metadata with save functionality
|
||||||
* Wraps DescriptionInput and provides save for title, description, name, year
|
* Wraps DescriptionInput and provides save for title, description, name, year
|
||||||
|
*
|
||||||
|
* @param mode - 'edit' (default) shows save/discard, 'upload' hides them, 'moderate' uses different API
|
||||||
*/
|
*/
|
||||||
function GroupMetadataEditor({
|
function GroupMetadataEditor({
|
||||||
initialMetadata,
|
initialMetadata,
|
||||||
|
metadata: externalMetadata,
|
||||||
|
onMetadataChange,
|
||||||
token,
|
token,
|
||||||
onRefresh
|
groupId,
|
||||||
|
onRefresh,
|
||||||
|
mode = 'edit'
|
||||||
}) {
|
}) {
|
||||||
const [metadata, setMetadata] = useState(initialMetadata || {
|
const [metadata, setMetadata] = useState(initialMetadata || {
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
|
|
@ -26,15 +32,22 @@ function GroupMetadataEditor({
|
||||||
});
|
});
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
// Update when initialMetadata changes
|
// In upload mode: use external state
|
||||||
|
const isUploadMode = mode === 'upload';
|
||||||
|
const isModerateMode = mode === 'moderate';
|
||||||
|
const currentMetadata = isUploadMode ? externalMetadata : metadata;
|
||||||
|
const setCurrentMetadata = isUploadMode ? onMetadataChange : setMetadata;
|
||||||
|
|
||||||
|
// Update when initialMetadata changes (edit mode only)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (initialMetadata) {
|
if (initialMetadata && !isUploadMode) {
|
||||||
setMetadata(initialMetadata);
|
setMetadata(initialMetadata);
|
||||||
setOriginalMetadata(initialMetadata);
|
setOriginalMetadata(initialMetadata);
|
||||||
}
|
}
|
||||||
}, [initialMetadata]);
|
}, [initialMetadata, isUploadMode]);
|
||||||
|
|
||||||
const hasChanges = () => {
|
const hasChanges = () => {
|
||||||
|
if (isUploadMode) return false; // No changes tracking in upload mode
|
||||||
return JSON.stringify(metadata) !== JSON.stringify(originalMetadata);
|
return JSON.stringify(metadata) !== JSON.stringify(originalMetadata);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -51,8 +64,15 @@ function GroupMetadataEditor({
|
||||||
try {
|
try {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
||||||
const res = await fetch(`/api/manage/${token}/metadata`, {
|
// Different API endpoints for manage vs moderate
|
||||||
method: 'PUT',
|
const endpoint = isModerateMode
|
||||||
|
? `/groups/${groupId}`
|
||||||
|
: `/api/manage/${token}/metadata`;
|
||||||
|
|
||||||
|
const method = isModerateMode ? 'PATCH' : 'PUT';
|
||||||
|
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
method: method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(metadata)
|
body: JSON.stringify(metadata)
|
||||||
});
|
});
|
||||||
|
|
@ -116,11 +136,11 @@ function GroupMetadataEditor({
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<DescriptionInput
|
<DescriptionInput
|
||||||
metadata={metadata}
|
metadata={currentMetadata}
|
||||||
onMetadataChange={setMetadata}
|
onMetadataChange={setCurrentMetadata}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasChanges() && (
|
{!isUploadMode && hasChanges() && (
|
||||||
<Box sx={{ mt: 2, display: 'flex', gap: 1, justifyContent: 'center' }}>
|
<Box sx={{ mt: 2, display: 'flex', gap: 1, justifyContent: 'center' }}>
|
||||||
<button
|
<button
|
||||||
className="btn btn-success"
|
className="btn btn-success"
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,17 @@ import ImageGallery from './ImageGallery';
|
||||||
/**
|
/**
|
||||||
* Manages image descriptions with save functionality
|
* Manages image descriptions with save functionality
|
||||||
* Wraps ImageGallery and provides batch save for all descriptions
|
* Wraps ImageGallery and provides batch save for all descriptions
|
||||||
|
*
|
||||||
|
* @param mode - 'manage' (uses token) or 'moderate' (uses groupId)
|
||||||
*/
|
*/
|
||||||
function ImageDescriptionManager({
|
function ImageDescriptionManager({
|
||||||
images,
|
images,
|
||||||
token,
|
token,
|
||||||
|
groupId,
|
||||||
enableReordering = false,
|
enableReordering = false,
|
||||||
onReorder,
|
onReorder,
|
||||||
onRefresh
|
onRefresh,
|
||||||
|
mode = 'manage'
|
||||||
}) {
|
}) {
|
||||||
const [imageDescriptions, setImageDescriptions] = useState({});
|
const [imageDescriptions, setImageDescriptions] = useState({});
|
||||||
const [originalDescriptions, setOriginalDescriptions] = useState({});
|
const [originalDescriptions, setOriginalDescriptions] = useState({});
|
||||||
|
|
@ -61,8 +65,15 @@ function ImageDescriptionManager({
|
||||||
description: description || null
|
description: description || null
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const res = await fetch(`/api/manage/${token}/images/descriptions`, {
|
// Different API endpoints for manage vs moderate
|
||||||
method: 'PUT',
|
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' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ descriptions })
|
body: JSON.stringify({ descriptions })
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
Container,
|
Container,
|
||||||
Card,
|
Card,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
|
||||||
Box,
|
Box,
|
||||||
CircularProgress
|
CircularProgress
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
@ -107,9 +106,9 @@ function GroupsOverviewPage() {
|
||||||
<Typography variant="body1" style={{ color: '#666666', marginBottom: '30px' }}>
|
<Typography variant="body1" style={{ color: '#666666', marginBottom: '30px' }}>
|
||||||
{error}
|
{error}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button onClick={loadGroups} className="primary-button">
|
<button onClick={loadGroups} className="btn btn-secondary">
|
||||||
🔄 Erneut versuchen
|
🔄 Erneut versuchen
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : groups.length === 0 ? (
|
) : groups.length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
|
|
@ -119,13 +118,13 @@ function GroupsOverviewPage() {
|
||||||
<Typography variant="body1" style={{ color: '#999999', marginBottom: '30px' }}>
|
<Typography variant="body1" style={{ color: '#999999', marginBottom: '30px' }}>
|
||||||
Erstellen Sie Ihre erste Slideshow, indem Sie mehrere Bilder hochladen.
|
Erstellen Sie Ihre erste Slideshow, indem Sie mehrere Bilder hochladen.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<button
|
||||||
className="primary-button"
|
className="btn btn-success"
|
||||||
onClick={handleCreateNew}
|
onClick={handleCreateNew}
|
||||||
size="large"
|
style={{ fontSize: '16px', padding: '12px 24px' }}
|
||||||
>
|
>
|
||||||
➕ Erste Slideshow erstellen
|
➕ Erste Slideshow erstellen
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,54 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Button, Container } from '@mui/material';
|
import { Container, Box } from '@mui/material';
|
||||||
import Swal from 'sweetalert2/dist/sweetalert2.js';
|
|
||||||
import 'sweetalert2/src/sweetalert2.scss';
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Navbar from '../ComponentUtils/Headers/Navbar';
|
import Navbar from '../ComponentUtils/Headers/Navbar';
|
||||||
import Footer from '../ComponentUtils/Footer';
|
import Footer from '../ComponentUtils/Footer';
|
||||||
import ImageGallery from '../ComponentUtils/ImageGallery';
|
import ImageDescriptionManager from '../ComponentUtils/ImageDescriptionManager';
|
||||||
import DescriptionInput from '../ComponentUtils/MultiUpload/DescriptionInput';
|
import GroupMetadataEditor from '../ComponentUtils/GroupMetadataEditor';
|
||||||
|
import Loading from '../ComponentUtils/LoadingAnimation/Loading';
|
||||||
// Services
|
|
||||||
import { updateImageOrder } from '../../services/reorderService';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModerationGroupImagesPage - Admin page for moderating group images
|
||||||
|
*
|
||||||
|
* Uses modular components:
|
||||||
|
* - ImageDescriptionManager: Edit image descriptions with batch save
|
||||||
|
* - GroupMetadataEditor: Edit group metadata with save/discard
|
||||||
|
*/
|
||||||
const ModerationGroupImagesPage = () => {
|
const ModerationGroupImagesPage = () => {
|
||||||
const { groupId } = useParams();
|
const { groupId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [group, setGroup] = useState(null);
|
const [group, setGroup] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
// selectedImages will hold objects compatible with ImagePreviewGallery
|
|
||||||
const [selectedImages, setSelectedImages] = useState([]);
|
|
||||||
const [metadata, setMetadata] = useState({ year: new Date().getFullYear(), title: '', description: '', name: '' });
|
|
||||||
const [isReordering, setIsReordering] = useState(false);
|
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
|
||||||
const [imageDescriptions, setImageDescriptions] = useState({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadGroup();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [groupId]);
|
|
||||||
|
|
||||||
const loadGroup = useCallback(async () => {
|
const loadGroup = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await fetch(`/moderation/groups/${groupId}`);
|
const res = await fetch(`/moderation/groups/${groupId}`);
|
||||||
if (!res.ok) throw new Error('Nicht gefunden');
|
if (!res.ok) throw new Error('Nicht gefunden');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setGroup(data);
|
|
||||||
|
|
||||||
// Map group's images to preview-friendly objects
|
// Transform data similar to ManagementPortalPage
|
||||||
if (data.images && data.images.length > 0) {
|
const transformedData = {
|
||||||
const mapped = data.images.map(img => ({
|
...data,
|
||||||
...img, // Pass all image fields including previewPath and imageDescription
|
metadata: {
|
||||||
remoteUrl: `/download/${img.fileName}`, // Keep for backward compatibility
|
|
||||||
originalName: img.originalName || img.fileName,
|
|
||||||
id: img.id
|
|
||||||
}));
|
|
||||||
setSelectedImages(mapped);
|
|
||||||
|
|
||||||
// Initialize descriptions from server
|
|
||||||
const descriptions = {};
|
|
||||||
data.images.forEach(img => {
|
|
||||||
if (img.imageDescription) {
|
|
||||||
descriptions[img.id] = img.imageDescription;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setImageDescriptions(descriptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate metadata from group
|
|
||||||
setMetadata({
|
|
||||||
year: data.year || new Date().getFullYear(),
|
year: data.year || new Date().getFullYear(),
|
||||||
title: data.title || '',
|
title: data.title || '',
|
||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
name: data.name || ''
|
name: data.name || ''
|
||||||
});
|
},
|
||||||
|
images: (data.images || []).map(img => ({
|
||||||
|
...img,
|
||||||
|
remoteUrl: `/download/${img.fileName}`,
|
||||||
|
originalName: img.originalName || img.fileName,
|
||||||
|
id: img.id,
|
||||||
|
imageDescription: img.imageDescription || ''
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
setGroup(transformedData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError('Fehler beim Laden der Gruppe');
|
setError('Fehler beim Laden der Gruppe');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -78,155 +56,12 @@ const ModerationGroupImagesPage = () => {
|
||||||
}
|
}
|
||||||
}, [groupId]);
|
}, [groupId]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
useEffect(() => {
|
||||||
if (!group) return;
|
loadGroup();
|
||||||
setSaving(true);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
try {
|
}, [groupId]);
|
||||||
// 1. Speichere Gruppen-Metadaten
|
|
||||||
const payload = {
|
|
||||||
title: metadata.title,
|
|
||||||
description: metadata.description,
|
|
||||||
year: metadata.year,
|
|
||||||
name: metadata.name
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await fetch(`/groups/${groupId}`, {
|
if (loading) return <Loading />;
|
||||||
method: 'PATCH',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const body = await res.json().catch(() => ({}));
|
|
||||||
throw new Error(body.message || 'Speichern der Metadaten fehlgeschlagen');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Speichere Bildbeschreibungen (falls vorhanden)
|
|
||||||
if (Object.keys(imageDescriptions).length > 0) {
|
|
||||||
const descriptions = Object.entries(imageDescriptions).map(([id, desc]) => ({
|
|
||||||
imageId: parseInt(id),
|
|
||||||
description: desc
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log('Speichere Beschreibungen:', descriptions);
|
|
||||||
|
|
||||||
const descRes = await fetch(`/groups/${groupId}/images/batch-description`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ descriptions })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!descRes.ok) {
|
|
||||||
const body = await descRes.json().catch(() => ({}));
|
|
||||||
throw new Error(body.message || 'Speichern der Beschreibungen fehlgeschlagen');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Swal.fire({ icon: 'success', title: 'Gruppe erfolgreich aktualisiert', timer: 1500, showConfirmButton: false });
|
|
||||||
navigate('/moderation');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
Swal.fire({ icon: 'error', title: 'Fehler beim Speichern', text: e.message });
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteImage = async (imageId) => {
|
|
||||||
if (!window.confirm('Bild wirklich löschen?')) return;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/groups/${groupId}/images/${imageId}`, { method: 'DELETE' });
|
|
||||||
if (!res.ok) throw new Error('Löschen fehlgeschlagen');
|
|
||||||
// Aktualisiere lokale Ansicht
|
|
||||||
const newImages = group.images.filter(img => img.id !== imageId);
|
|
||||||
setGroup({ ...group, images: newImages, imageCount: (group.imageCount || 0) - 1 });
|
|
||||||
setSelectedImages(prev => prev.filter(img => img.id !== imageId));
|
|
||||||
Swal.fire({ icon: 'success', title: 'Bild gelöscht', timer: 1200, showConfirmButton: false });
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
Swal.fire({ icon: 'error', title: 'Fehler beim Löschen des Bildes' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveImage = (indexToRemove) => {
|
|
||||||
// If it's a remote image mapped with id, call delete
|
|
||||||
const img = selectedImages[indexToRemove];
|
|
||||||
if (img && img.id) {
|
|
||||||
handleDeleteImage(img.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedImages(prev => prev.filter((_, index) => index !== indexToRemove));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle drag-and-drop reordering
|
|
||||||
const handleReorder = useCallback(async (reorderedItems) => {
|
|
||||||
if (isReordering) return; // Prevent concurrent reordering
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsReordering(true);
|
|
||||||
const imageIds = reorderedItems.map(img => img.id);
|
|
||||||
|
|
||||||
// Update local state immediately (optimistic update)
|
|
||||||
setSelectedImages(reorderedItems);
|
|
||||||
|
|
||||||
// Also update group state to keep consistency
|
|
||||||
if (group) {
|
|
||||||
setGroup({ ...group, images: reorderedItems });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send API request
|
|
||||||
await updateImageOrder(groupId, imageIds);
|
|
||||||
|
|
||||||
// Show success feedback
|
|
||||||
Swal.fire({
|
|
||||||
icon: 'success',
|
|
||||||
title: 'Reihenfolge gespeichert',
|
|
||||||
timer: 1500,
|
|
||||||
showConfirmButton: false,
|
|
||||||
toast: true,
|
|
||||||
position: 'top-end'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Fehler beim Neuordnen:', error);
|
|
||||||
|
|
||||||
// Rollback on error - reload original order
|
|
||||||
await loadGroup();
|
|
||||||
|
|
||||||
Swal.fire({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Fehler beim Speichern',
|
|
||||||
text: 'Reihenfolge konnte nicht gespeichert werden',
|
|
||||||
timer: 3000,
|
|
||||||
showConfirmButton: false
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsReordering(false);
|
|
||||||
}
|
|
||||||
}, [groupId, group, isReordering, loadGroup]);
|
|
||||||
|
|
||||||
// Handle edit mode toggle
|
|
||||||
const handleEditMode = (enabled) => {
|
|
||||||
console.log('🔄 Edit mode toggled:', enabled ? 'ENABLED' : 'DISABLED');
|
|
||||||
setIsEditMode(enabled);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle description changes
|
|
||||||
const handleDescriptionChange = (imageId, description) => {
|
|
||||||
console.log('✏️ Description changed for image', imageId, ':', description);
|
|
||||||
setImageDescriptions(prev => {
|
|
||||||
const newDescriptions = {
|
|
||||||
...prev,
|
|
||||||
[imageId]: description.slice(0, 200) // Enforce max length
|
|
||||||
};
|
|
||||||
console.log('📝 Updated imageDescriptions:', newDescriptions);
|
|
||||||
return newDescriptions;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Note: approve/delete group actions are intentionally removed from this page
|
|
||||||
|
|
||||||
if (loading) return <div className="moderation-loading">Lade Gruppe...</div>;
|
|
||||||
if (error) return <div className="moderation-error">{error}</div>;
|
if (error) return <div className="moderation-error">{error}</div>;
|
||||||
if (!group) return <div className="moderation-error">Gruppe nicht gefunden</div>;
|
if (!group) return <div className="moderation-error">Gruppe nicht gefunden</div>;
|
||||||
|
|
||||||
|
|
@ -234,47 +69,37 @@ const ModerationGroupImagesPage = () => {
|
||||||
<div className="allContainer">
|
<div className="allContainer">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<Container maxWidth="lg" className="page-container">
|
<Container maxWidth="lg" sx={{ pt: '20px', pb: '40px', minHeight: '80vh' }}>
|
||||||
<ImageGallery
|
{/* Image Descriptions Manager */}
|
||||||
items={selectedImages}
|
<ImageDescriptionManager
|
||||||
onDelete={handleRemoveImage}
|
images={group.images}
|
||||||
onReorder={handleReorder}
|
groupId={groupId}
|
||||||
enableReordering={true}
|
onRefresh={loadGroup}
|
||||||
isReordering={isReordering}
|
mode="moderate"
|
||||||
mode="preview"
|
|
||||||
showActions={true}
|
|
||||||
isEditMode={isEditMode}
|
|
||||||
onEditMode={handleEditMode}
|
|
||||||
imageDescriptions={imageDescriptions}
|
|
||||||
onDescriptionChange={handleDescriptionChange}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedImages.length > 0 && (
|
{/* Group Metadata Editor */}
|
||||||
<>
|
<GroupMetadataEditor
|
||||||
<DescriptionInput metadata={metadata} onMetadataChange={setMetadata} />
|
initialMetadata={group.metadata}
|
||||||
|
groupId={groupId}
|
||||||
|
onRefresh={loadGroup}
|
||||||
|
mode="moderate"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="action-buttons">
|
{/* Back Button */}
|
||||||
<Button className="btn btn-secondary" onClick={() => navigate('/moderation')}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
|
||||||
↩ Zurück
|
<button
|
||||||
</Button>
|
className="btn btn-secondary"
|
||||||
<Button
|
onClick={() => navigate('/moderation')}
|
||||||
className="btn btn-success"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={saving}
|
|
||||||
style={{ minWidth: '160px' }}
|
|
||||||
>
|
>
|
||||||
{saving ? '⏳ Speichern...' : '💾 Speichern'}
|
↩ Zurück zur Übersicht
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</Box>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<div className="footerContainer"><Footer /></div>
|
<div className="footerContainer"><Footer /></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ModerationGroupImagesPage;
|
export default ModerationGroupImagesPage;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Container, Box, FormControl, InputLabel, Select, MenuItem, Button } from '@mui/material';
|
import { Container, Box, FormControl, InputLabel, Select, MenuItem } from '@mui/material';
|
||||||
import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
|
||||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||||
import Swal from 'sweetalert2/dist/sweetalert2.js';
|
import Swal from 'sweetalert2/dist/sweetalert2.js';
|
||||||
import Navbar from '../ComponentUtils/Headers/Navbar';
|
import Navbar from '../ComponentUtils/Headers/Navbar';
|
||||||
|
|
@ -298,17 +297,16 @@ const ModerationGroupsPage = () => {
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<Button
|
<button
|
||||||
variant="contained"
|
className="btn btn-success"
|
||||||
startIcon={<FileDownloadIcon />}
|
|
||||||
onClick={exportConsentData}
|
onClick={exportConsentData}
|
||||||
sx={{
|
style={{
|
||||||
bgcolor: '#2196F3',
|
fontSize: '14px',
|
||||||
'&:hover': { bgcolor: '#1976D2' }
|
padding: '10px 20px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Consent-Daten exportieren
|
📥 Consent-Daten exportieren
|
||||||
</Button>
|
</button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Wartende Gruppen */}
|
{/* Wartende Gruppen */}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,23 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Button, Card, CardContent, Typography, Container, Box } from '@mui/material';
|
import { Card, CardContent, Typography, Container, Box } from '@mui/material';
|
||||||
import Swal from 'sweetalert2/dist/sweetalert2.js';
|
|
||||||
import 'sweetalert2/src/sweetalert2.scss';
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Navbar from '../ComponentUtils/Headers/Navbar';
|
import Navbar from '../ComponentUtils/Headers/Navbar';
|
||||||
import Footer from '../ComponentUtils/Footer';
|
import Footer from '../ComponentUtils/Footer';
|
||||||
import MultiImageDropzone from '../ComponentUtils/MultiUpload/MultiImageDropzone';
|
import MultiImageDropzone from '../ComponentUtils/MultiUpload/MultiImageDropzone';
|
||||||
import ImageGallery from '../ComponentUtils/ImageGallery';
|
import ImageGallery from '../ComponentUtils/ImageGallery';
|
||||||
import DescriptionInput from '../ComponentUtils/MultiUpload/DescriptionInput';
|
import GroupMetadataEditor from '../ComponentUtils/GroupMetadataEditor';
|
||||||
|
import ConsentManager from '../ComponentUtils/ConsentManager';
|
||||||
import UploadProgress from '../ComponentUtils/MultiUpload/UploadProgress';
|
import UploadProgress from '../ComponentUtils/MultiUpload/UploadProgress';
|
||||||
import Loading from '../ComponentUtils/LoadingAnimation/Loading';
|
import Loading from '../ComponentUtils/LoadingAnimation/Loading';
|
||||||
import ConsentCheckboxes from '../ComponentUtils/MultiUpload/ConsentCheckboxes';
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { uploadImageBatch } from '../../Utils/batchUpload';
|
import { uploadImageBatch } from '../../Utils/batchUpload';
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
import '../../App.css';
|
import '../../App.css';
|
||||||
// Background.css is now globally imported in src/index.js
|
|
||||||
|
|
||||||
// Styles migrated to MUI sx props in-place below
|
|
||||||
|
|
||||||
function MultiUploadPage() {
|
function MultiUploadPage() {
|
||||||
|
|
||||||
const [selectedImages, setSelectedImages] = useState([]);
|
const [selectedImages, setSelectedImages] = useState([]);
|
||||||
const [metadata, setMetadata] = useState({
|
const [metadata, setMetadata] = useState({
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
|
|
@ -54,29 +48,23 @@ function MultiUploadPage() {
|
||||||
}, [selectedImages]);
|
}, [selectedImages]);
|
||||||
|
|
||||||
const handleImagesSelected = (newImages) => {
|
const handleImagesSelected = (newImages) => {
|
||||||
console.log('handleImagesSelected called with:', newImages);
|
|
||||||
|
|
||||||
// Convert File objects to preview objects with URLs
|
// Convert File objects to preview objects with URLs
|
||||||
const imageObjects = newImages.map((file, index) => ({
|
const imageObjects = newImages.map((file, index) => ({
|
||||||
id: `preview-${Date.now()}-${index}`, // Unique ID für Preview-Modus
|
id: `preview-${Date.now()}-${index}`,
|
||||||
file: file, // Original File object for upload
|
file: file,
|
||||||
url: URL.createObjectURL(file), // Preview URL
|
url: URL.createObjectURL(file),
|
||||||
name: file.name,
|
name: file.name,
|
||||||
originalName: file.name,
|
originalName: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
type: file.type
|
type: file.type
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setSelectedImages(prev => {
|
setSelectedImages(prev => [...prev, ...imageObjects]);
|
||||||
const updated = [...prev, ...imageObjects];
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveImage = (indexToRemove) => {
|
const handleRemoveImage = (indexToRemove) => {
|
||||||
setSelectedImages(prev => {
|
setSelectedImages(prev => {
|
||||||
const imageToRemove = prev[indexToRemove];
|
const imageToRemove = prev[indexToRemove];
|
||||||
// Clean up the object URL to avoid memory leaks
|
|
||||||
if (imageToRemove && imageToRemove.url && imageToRemove.url.startsWith('blob:')) {
|
if (imageToRemove && imageToRemove.url && imageToRemove.url.startsWith('blob:')) {
|
||||||
URL.revokeObjectURL(imageToRemove.url);
|
URL.revokeObjectURL(imageToRemove.url);
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +73,6 @@ function MultiUploadPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearAll = () => {
|
const handleClearAll = () => {
|
||||||
// Clean up all object URLs
|
|
||||||
selectedImages.forEach(img => {
|
selectedImages.forEach(img => {
|
||||||
if (img.url && img.url.startsWith('blob:')) {
|
if (img.url && img.url.startsWith('blob:')) {
|
||||||
URL.revokeObjectURL(img.url);
|
URL.revokeObjectURL(img.url);
|
||||||
|
|
@ -107,105 +94,71 @@ function MultiUploadPage() {
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle drag-and-drop reordering (only updates local state, no API call)
|
|
||||||
const handleReorder = (reorderedItems) => {
|
const handleReorder = (reorderedItems) => {
|
||||||
console.log('Reordering images in preview:', reorderedItems);
|
|
||||||
setSelectedImages(reorderedItems);
|
setSelectedImages(reorderedItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle edit mode toggle
|
|
||||||
const handleEditMode = (enabled) => {
|
const handleEditMode = (enabled) => {
|
||||||
setIsEditMode(enabled);
|
setIsEditMode(enabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle description changes
|
|
||||||
const handleDescriptionChange = (imageId, description) => {
|
const handleDescriptionChange = (imageId, description) => {
|
||||||
setImageDescriptions(prev => ({
|
setImageDescriptions(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[imageId]: description.slice(0, 200) // Enforce max length
|
[imageId]: description.slice(0, 200)
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (selectedImages.length === 0) {
|
if (selectedImages.length === 0) return;
|
||||||
Swal.fire({
|
|
||||||
icon: 'warning',
|
|
||||||
title: 'Keine Bilder ausgewählt',
|
|
||||||
text: 'Bitte wählen Sie mindestens ein Bild zum Upload aus.',
|
|
||||||
confirmButtonColor: '#4CAF50'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!metadata.year || !metadata.title.trim()) {
|
if (!metadata.year || !metadata.title.trim()) return;
|
||||||
Swal.fire({
|
|
||||||
icon: 'warning',
|
|
||||||
title: 'Pflichtfelder fehlen',
|
|
||||||
text: 'Bitte gebe das Jahr und den Titel an.',
|
|
||||||
confirmButtonColor: '#4CAF50'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GDPR: Validate workshop consent (mandatory)
|
if (!consents.workshopConsent) return;
|
||||||
if (!consents.workshopConsent) {
|
|
||||||
Swal.fire({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Einwilligung erforderlich',
|
|
||||||
text: 'Die Zustimmung zur Anzeige in der Werkstatt ist erforderlich.',
|
|
||||||
confirmButtonColor: '#f44336'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Simuliere Progress (da wir noch keinen echten Progress haben)
|
const filesToUpload = selectedImages.map(img => img.file).filter(Boolean);
|
||||||
const progressInterval = setInterval(() => {
|
|
||||||
setUploadProgress(prev => {
|
if (filesToUpload.length === 0) {
|
||||||
if (prev >= 90) {
|
throw new Error('Keine gültigen Bilder zum Upload');
|
||||||
clearInterval(progressInterval);
|
}
|
||||||
return 90;
|
|
||||||
|
// Map preview IDs to actual file names for backend
|
||||||
|
const descriptionsForUpload = {};
|
||||||
|
selectedImages.forEach(img => {
|
||||||
|
if (imageDescriptions[img.id]) {
|
||||||
|
descriptionsForUpload[img.originalName] = imageDescriptions[img.id];
|
||||||
}
|
}
|
||||||
return prev + 10;
|
|
||||||
});
|
});
|
||||||
}, 200);
|
|
||||||
|
|
||||||
// Extract the actual File objects from our image objects
|
const result = await uploadImageBatch({
|
||||||
const filesToUpload = selectedImages.map(img => img.file || img);
|
images: filesToUpload,
|
||||||
|
metadata,
|
||||||
|
imageDescriptions: descriptionsForUpload,
|
||||||
|
consents,
|
||||||
|
onProgress: setUploadProgress
|
||||||
|
});
|
||||||
|
|
||||||
// Prepare descriptions array for backend
|
|
||||||
const descriptionsArray = selectedImages.map(img => ({
|
|
||||||
fileName: img.name,
|
|
||||||
description: imageDescriptions[img.id] || ''
|
|
||||||
}));
|
|
||||||
|
|
||||||
const result = await uploadImageBatch(filesToUpload, metadata, descriptionsArray, consents);
|
|
||||||
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
setUploadProgress(100);
|
|
||||||
|
|
||||||
// Show success content
|
|
||||||
setTimeout(() => {
|
|
||||||
setUploadComplete(true);
|
setUploadComplete(true);
|
||||||
setUploadResult(result);
|
setUploadResult(result);
|
||||||
}, 500);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setUploading(false);
|
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
|
setUploading(false);
|
||||||
Swal.fire({
|
setUploadComplete(false);
|
||||||
icon: 'error',
|
|
||||||
title: 'Upload fehlgeschlagen',
|
|
||||||
text: error.message || 'Ein Fehler ist beim Upload aufgetreten.',
|
|
||||||
confirmButtonColor: '#f44336'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canUpload = () => {
|
||||||
|
return selectedImages.length > 0 &&
|
||||||
|
metadata.year &&
|
||||||
|
metadata.title.trim() &&
|
||||||
|
consents.workshopConsent;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="allContainer">
|
<div className="allContainer">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
@ -224,11 +177,14 @@ function MultiUploadPage() {
|
||||||
|
|
||||||
{!uploading ? (
|
{!uploading ? (
|
||||||
<>
|
<>
|
||||||
|
{/* Image Dropzone - stays inline as it's upload-specific */}
|
||||||
<MultiImageDropzone
|
<MultiImageDropzone
|
||||||
onImagesSelected={handleImagesSelected}
|
onImagesSelected={handleImagesSelected}
|
||||||
selectedImages={selectedImages}
|
selectedImages={selectedImages}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Image Gallery with descriptions */}
|
||||||
|
{selectedImages.length > 0 && (
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
items={selectedImages}
|
items={selectedImages}
|
||||||
onDelete={handleRemoveImage}
|
onDelete={handleRemoveImage}
|
||||||
|
|
@ -241,76 +197,50 @@ function MultiUploadPage() {
|
||||||
imageDescriptions={imageDescriptions}
|
imageDescriptions={imageDescriptions}
|
||||||
onDescriptionChange={handleDescriptionChange}
|
onDescriptionChange={handleDescriptionChange}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedImages.length > 0 && (
|
{selectedImages.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<DescriptionInput
|
{/* Modular Components like ManagementPortalPage */}
|
||||||
|
<GroupMetadataEditor
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
onMetadataChange={setMetadata}
|
onMetadataChange={setMetadata}
|
||||||
|
mode="upload"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConsentCheckboxes
|
<ConsentManager
|
||||||
consents={consents}
|
consents={consents}
|
||||||
onConsentChange={setConsents}
|
onConsentsChange={setConsents}
|
||||||
disabled={uploading}
|
mode="upload"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: '15px', justifyContent: 'center', mt: '20px', flexWrap: 'wrap' }}>
|
{/* Action Buttons */}
|
||||||
<Button
|
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'center', mt: 3, flexWrap: 'wrap' }}>
|
||||||
sx={{
|
<button
|
||||||
borderRadius: '25px',
|
className="btn btn-success"
|
||||||
px: '30px',
|
|
||||||
py: '12px',
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 500,
|
|
||||||
textTransform: 'none',
|
|
||||||
background: 'linear-gradient(45deg, #4CAF50 30%, #45a049 90%)',
|
|
||||||
color: 'white',
|
|
||||||
'&:hover': {
|
|
||||||
background: 'linear-gradient(45deg, #45a049 30%, #4CAF50 90%)',
|
|
||||||
transform: 'translateY(-2px)',
|
|
||||||
boxShadow: '0 4px 12px rgba(76, 175, 80, 0.3)'
|
|
||||||
},
|
|
||||||
'&.Mui-disabled': {
|
|
||||||
background: '#cccccc',
|
|
||||||
color: '#666666'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={uploading || selectedImages.length === 0 || !consents.workshopConsent}
|
disabled={!canUpload()}
|
||||||
size="large"
|
style={{
|
||||||
|
fontSize: '16px',
|
||||||
|
padding: '12px 30px'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
🚀 {selectedImages.length} Bild{selectedImages.length !== 1 ? 'er' : ''} hochladen
|
🚀 {selectedImages.length} Bild{selectedImages.length !== 1 ? 'er' : ''} hochladen
|
||||||
</Button>
|
</button>
|
||||||
|
|
||||||
<Button
|
<button
|
||||||
sx={{
|
className="btn btn-secondary"
|
||||||
borderRadius: '25px',
|
|
||||||
px: '30px',
|
|
||||||
py: '12px',
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 500,
|
|
||||||
textTransform: 'none',
|
|
||||||
border: '2px solid #f44336',
|
|
||||||
color: '#f44336',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: '#f44336',
|
|
||||||
color: 'white',
|
|
||||||
transform: 'translateY(-2px)',
|
|
||||||
boxShadow: '0 4px 12px rgba(244, 67, 54, 0.3)'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={handleClearAll}
|
onClick={handleClearAll}
|
||||||
size="large"
|
style={{
|
||||||
|
fontSize: '16px',
|
||||||
|
padding: '12px 30px'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
🗑️ Alle entfernen
|
🗑️ Alle entfernen
|
||||||
</Button>
|
</button>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
||||||
|
|
@ -397,34 +327,19 @@ function MultiUploadPage() {
|
||||||
}}>
|
}}>
|
||||||
{window.location.origin}/manage/{uploadResult.managementToken}
|
{window.location.origin}/manage/{uploadResult.managementToken}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<button
|
||||||
size="small"
|
className="btn btn-secondary"
|
||||||
sx={{
|
style={{
|
||||||
minWidth: 'auto',
|
|
||||||
px: 2,
|
|
||||||
py: 0.5,
|
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
textTransform: 'none',
|
padding: '6px 16px'
|
||||||
bgcolor: '#1976d2',
|
|
||||||
color: 'white',
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: '#1565c0'
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const link = `${window.location.origin}/manage/${uploadResult.managementToken}`;
|
const link = `${window.location.origin}/manage/${uploadResult.managementToken}`;
|
||||||
navigator.clipboard.writeText(link);
|
navigator.clipboard.writeText(link);
|
||||||
Swal.fire({
|
|
||||||
icon: 'success',
|
|
||||||
title: 'Link kopiert!',
|
|
||||||
text: 'Der Verwaltungslink wurde in die Zwischenablage kopiert.',
|
|
||||||
timer: 2000,
|
|
||||||
showConfirmButton: false
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
📋 Kopieren
|
📋 Kopieren
|
||||||
</Button>
|
</button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography sx={{ fontSize: '11px', color: '#666', mb: 0.5 }}>
|
<Typography sx={{ fontSize: '11px', color: '#666', mb: 0.5 }}>
|
||||||
|
|
@ -445,27 +360,16 @@ function MultiUploadPage() {
|
||||||
<strong>Fragen oder Widerruf?</strong> Kontakt: <strong>it@hobbyhimmel.de</strong>
|
<strong>Fragen oder Widerruf?</strong> Kontakt: <strong>it@hobbyhimmel.de</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Button
|
<button
|
||||||
sx={{
|
className="btn btn-success"
|
||||||
background: 'white',
|
style={{
|
||||||
color: '#4CAF50',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
px: 4,
|
padding: '12px 30px'
|
||||||
py: 1.5,
|
|
||||||
borderRadius: '25px',
|
|
||||||
textTransform: 'none',
|
|
||||||
'&:hover': {
|
|
||||||
background: '#f0f0f0',
|
|
||||||
transform: 'scale(1.05)',
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.2)'
|
|
||||||
},
|
|
||||||
transition: 'all 0.3s ease'
|
|
||||||
}}
|
}}
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
>
|
>
|
||||||
👍 Weitere Bilder hochladen
|
👍 Weitere Bilder hochladen
|
||||||
</Button>
|
</button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ export const uploadImageBatch = async ({ images, metadata, imageDescriptions = {
|
||||||
// Füge Metadaten hinzu
|
// Füge Metadaten hinzu
|
||||||
formData.append('metadata', JSON.stringify(metadata || {}));
|
formData.append('metadata', JSON.stringify(metadata || {}));
|
||||||
|
|
||||||
// Füge Beschreibungen hinzu (convert object to array format)
|
// Füge Beschreibungen hinzu (convert object to array format with fileName)
|
||||||
const descriptionsArray = Object.entries(imageDescriptions).map(([id, description]) => ({
|
const descriptionsArray = Object.entries(imageDescriptions).map(([fileName, description]) => ({
|
||||||
imageId: id,
|
fileName: fileName,
|
||||||
description
|
description
|
||||||
}));
|
}));
|
||||||
if (descriptionsArray.length > 0) {
|
if (descriptionsArray.length > 0) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user