feat(frontend): Add consent management UI components
- Add ConsentCheckboxes component with workshop and social media consents - Add UploadSuccessDialog with group ID display and copy functionality - Integrate consent validation into MultiUploadPage - Extend batchUpload utility to send consent data - Add GDPR compliance notices and contact information - Block uploads without required workshop consent
This commit is contained in:
parent
6ba7f7bd33
commit
39f133eadf
|
|
@ -0,0 +1,206 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
FormControlLabel,
|
||||||
|
Checkbox,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
Divider,
|
||||||
|
Alert
|
||||||
|
} from '@mui/material';
|
||||||
|
import InfoIcon from '@mui/icons-material/Info';
|
||||||
|
import FacebookIcon from '@mui/icons-material/Facebook';
|
||||||
|
import InstagramIcon from '@mui/icons-material/Instagram';
|
||||||
|
import MusicNoteIcon from '@mui/icons-material/MusicNote';
|
||||||
|
|
||||||
|
const ICON_MAP = {
|
||||||
|
'Facebook': FacebookIcon,
|
||||||
|
'Instagram': InstagramIcon,
|
||||||
|
'MusicNote': MusicNoteIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentCheckboxes Component
|
||||||
|
*
|
||||||
|
* GDPR-konforme Einwilligungsabfrage für Bildveröffentlichung
|
||||||
|
* - Pflicht: Werkstatt-Anzeige Zustimmung
|
||||||
|
* - Optional: Social Media Plattform-Zustimmungen
|
||||||
|
*/
|
||||||
|
function ConsentCheckboxes({ onConsentChange, consents, disabled = false }) {
|
||||||
|
const [platforms, setPlatforms] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Lade verfügbare Plattformen vom Backend
|
||||||
|
fetchPlatforms();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchPlatforms = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/social-media/platforms');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load platforms');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setPlatforms(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading platforms:', error);
|
||||||
|
setError('Plattformen konnten nicht geladen werden');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWorkshopChange = (event) => {
|
||||||
|
onConsentChange({
|
||||||
|
...consents,
|
||||||
|
workshopConsent: event.target.checked
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSocialMediaChange = (platformId) => (event) => {
|
||||||
|
const updatedConsents = { ...consents };
|
||||||
|
const platformConsents = updatedConsents.socialMediaConsents || [];
|
||||||
|
|
||||||
|
if (event.target.checked) {
|
||||||
|
// Füge Consent hinzu
|
||||||
|
platformConsents.push({ platformId, consented: true });
|
||||||
|
} else {
|
||||||
|
// Entferne Consent
|
||||||
|
const index = platformConsents.findIndex(c => c.platformId === platformId);
|
||||||
|
if (index > -1) {
|
||||||
|
platformConsents.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedConsents.socialMediaConsents = platformConsents;
|
||||||
|
onConsentChange(updatedConsents);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPlatformChecked = (platformId) => {
|
||||||
|
return consents.socialMediaConsents?.some(c => c.platformId === platformId) || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
mb: 3,
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||||
|
border: '2px solid #e0e0e0'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Aufklärungshinweis */}
|
||||||
|
<Alert severity="info" icon={<InfoIcon />} sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||||
|
Wichtiger Hinweis
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Alle hochgeladenen Inhalte werden vom Hobbyhimmel-Team geprüft, bevor sie
|
||||||
|
angezeigt oder veröffentlicht werden. Wir behalten uns vor, Inhalte nicht
|
||||||
|
zu zeigen oder rechtswidrige Inhalte zu entfernen.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
|
Nach dem Upload erhalten Sie eine Gruppen-ID als Referenz für die Kontaktaufnahme.
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* Pflicht-Zustimmung: Werkstatt-Anzeige */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600, color: '#333' }}>
|
||||||
|
Anzeige in der Werkstatt *
|
||||||
|
</Typography>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={consents.workshopConsent || false}
|
||||||
|
onChange={handleWorkshopChange}
|
||||||
|
disabled={disabled}
|
||||||
|
required
|
||||||
|
sx={{
|
||||||
|
color: '#4CAF50',
|
||||||
|
'&.Mui-checked': { color: '#4CAF50' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography variant="body2" sx={{ color: '#555' }}>
|
||||||
|
Ich willige ein, dass meine hochgeladenen Bilder auf dem Monitor in
|
||||||
|
der offenen Werkstatt des Hobbyhimmels angezeigt werden dürfen.
|
||||||
|
Die Bilder sind nur lokal im Hobbyhimmel sichtbar und werden nicht
|
||||||
|
über das Internet zugänglich gemacht. <strong>(Pflichtfeld)</strong>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
|
{/* Optional: Social Media Veröffentlichung */}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600, color: '#333' }}>
|
||||||
|
Social Media Veröffentlichung (optional)
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mb: 2, color: '#666' }}>
|
||||||
|
Ich willige ein, dass meine Bilder und Texte auf folgenden Social Media
|
||||||
|
Plattformen veröffentlicht werden dürfen (inklusive aller angegebenen
|
||||||
|
Informationen wie Name und Beschreibung):
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Typography sx={{ color: '#666', fontStyle: 'italic' }}>
|
||||||
|
Lade Plattformen...
|
||||||
|
</Typography>
|
||||||
|
) : error ? (
|
||||||
|
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
{platforms.map(platform => {
|
||||||
|
const IconComponent = ICON_MAP[platform.icon_name] || InfoIcon;
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
key={platform.id}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={isPlatformChecked(platform.id)}
|
||||||
|
onChange={handleSocialMediaChange(platform.id)}
|
||||||
|
disabled={disabled}
|
||||||
|
sx={{
|
||||||
|
color: '#2196F3',
|
||||||
|
'&.Mui-checked': { color: '#2196F3' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<IconComponent fontSize="small" sx={{ color: '#2196F3' }} />
|
||||||
|
<Typography variant="body2">
|
||||||
|
{platform.display_name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Widerrufs-Hinweis */}
|
||||||
|
<Alert severity="info" sx={{ mt: 3 }}>
|
||||||
|
<Typography variant="caption" sx={{ display: 'block' }}>
|
||||||
|
<strong>Widerruf Ihrer Einwilligung:</strong> Sie können Ihre Einwilligung
|
||||||
|
jederzeit widerrufen. Kontaktieren Sie uns hierzu mit Ihrer Gruppen-ID unter:{' '}
|
||||||
|
<strong>it@hobbyhimmel.de</strong>
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConsentCheckboxes;
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
Alert,
|
||||||
|
Tooltip,
|
||||||
|
Divider
|
||||||
|
} from '@mui/material';
|
||||||
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UploadSuccessDialog Component
|
||||||
|
*
|
||||||
|
* Zeigt Erfolgsmeldung nach Upload mit:
|
||||||
|
* - Gruppen-ID (kopierbar)
|
||||||
|
* - Anzahl hochgeladener Bilder
|
||||||
|
* - GDPR Kontaktinformationen
|
||||||
|
* - Hinweis auf Moderation
|
||||||
|
*/
|
||||||
|
function UploadSuccessDialog({ open, onClose, groupId, uploadCount }) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopyGroupId = () => {
|
||||||
|
navigator.clipboard.writeText(groupId).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: '0 8px 32px rgba(0,0,0,0.2)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header mit Schließen-Button */}
|
||||||
|
<DialogTitle sx={{ pb: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<CheckCircleIcon sx={{ color: '#4CAF50', fontSize: 32 }} />
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 600, color: '#333' }}>
|
||||||
|
Upload erfolgreich!
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton onClick={onClose} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent sx={{ pb: 3 }}>
|
||||||
|
{/* Success Message */}
|
||||||
|
<Alert severity="success" sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>{uploadCount}</strong> {uploadCount === 1 ? 'Bild wurde' : 'Bilder wurden'} erfolgreich hochgeladen
|
||||||
|
und werden nach der Prüfung durch das Hobbyhimmel-Team angezeigt.
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* Gruppen-ID Anzeige */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mb: 1, color: '#666', fontWeight: 600 }}>
|
||||||
|
Ihre Referenz-Nummer:
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
p: 2,
|
||||||
|
bgcolor: '#f5f5f5',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '2px solid #e0e0e0'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
color: '#1976d2',
|
||||||
|
fontWeight: 600
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{groupId}
|
||||||
|
</Typography>
|
||||||
|
<Tooltip title={copied ? 'Kopiert!' : 'Kopieren'}>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleCopyGroupId}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
bgcolor: copied ? '#4CAF50' : '#e0e0e0',
|
||||||
|
color: copied ? '#fff' : '#666',
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: copied ? '#45a049' : '#d0d0d0'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContentCopyIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" sx={{ display: 'block', mt: 1, color: '#666' }}>
|
||||||
|
Notieren Sie sich diese Nummer für spätere Anfragen an das Hobbyhimmel-Team.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|
||||||
|
{/* Nächste Schritte */}
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mb: 1.5, fontWeight: 600, color: '#333' }}>
|
||||||
|
Was passiert jetzt?
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Typography variant="body2" sx={{ color: '#666', minWidth: '20px' }}>•</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: '#666' }}>
|
||||||
|
Ihre Bilder werden vom Team geprüft
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Typography variant="body2" sx={{ color: '#666', minWidth: '20px' }}>•</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: '#666' }}>
|
||||||
|
Nach Freigabe erscheinen sie auf dem Werkstatt-Monitor
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Typography variant="body2" sx={{ color: '#666', minWidth: '20px' }}>•</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: '#666' }}>
|
||||||
|
Bei gewählter Social Media Einwilligung werden sie entsprechend veröffentlicht
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* GDPR Kontakt-Info */}
|
||||||
|
<Alert severity="info" sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="caption" sx={{ display: 'block', mb: 0.5 }}>
|
||||||
|
<strong>Fragen oder Widerruf Ihrer Einwilligung?</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption">
|
||||||
|
Kontaktieren Sie uns mit Ihrer Referenz-Nummer unter:{' '}
|
||||||
|
<strong>it@hobbyhimmel.de</strong>
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions sx={{ px: 3, pb: 3 }}>
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#1976d2',
|
||||||
|
'&:hover': { bgcolor: '#1565c0' },
|
||||||
|
textTransform: 'none',
|
||||||
|
fontWeight: 600,
|
||||||
|
py: 1.5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Schließen
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UploadSuccessDialog;
|
||||||
|
|
@ -11,6 +11,8 @@ import ImageGallery from '../ComponentUtils/ImageGallery';
|
||||||
import DescriptionInput from '../ComponentUtils/MultiUpload/DescriptionInput';
|
import DescriptionInput from '../ComponentUtils/MultiUpload/DescriptionInput';
|
||||||
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';
|
||||||
|
import UploadSuccessDialog from '../ComponentUtils/MultiUpload/UploadSuccessDialog';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { uploadImageBatch } from '../../Utils/batchUpload';
|
import { uploadImageBatch } from '../../Utils/batchUpload';
|
||||||
|
|
@ -30,12 +32,17 @@ function MultiUploadPage() {
|
||||||
description: '',
|
description: '',
|
||||||
name: ''
|
name: ''
|
||||||
});
|
});
|
||||||
|
const [consents, setConsents] = useState({
|
||||||
|
workshopConsent: false,
|
||||||
|
socialMediaConsents: []
|
||||||
|
});
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [uploadComplete, setUploadComplete] = useState(false);
|
const [uploadComplete, setUploadComplete] = useState(false);
|
||||||
const [uploadResult, setUploadResult] = useState(null);
|
const [uploadResult, setUploadResult] = useState(null);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const [imageDescriptions, setImageDescriptions] = useState({});
|
const [imageDescriptions, setImageDescriptions] = useState({});
|
||||||
|
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
|
||||||
|
|
||||||
// Cleanup object URLs when component unmounts
|
// Cleanup object URLs when component unmounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -94,6 +101,10 @@ function MultiUploadPage() {
|
||||||
description: '',
|
description: '',
|
||||||
name: ''
|
name: ''
|
||||||
});
|
});
|
||||||
|
setConsents({
|
||||||
|
workshopConsent: false,
|
||||||
|
socialMediaConsents: []
|
||||||
|
});
|
||||||
setImageDescriptions({});
|
setImageDescriptions({});
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
};
|
};
|
||||||
|
|
@ -138,6 +149,17 @@ function MultiUploadPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GDPR: Validate workshop consent (mandatory)
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
@ -162,15 +184,16 @@ function MultiUploadPage() {
|
||||||
description: imageDescriptions[img.id] || ''
|
description: imageDescriptions[img.id] || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const result = await uploadImageBatch(filesToUpload, metadata, descriptionsArray);
|
const result = await uploadImageBatch(filesToUpload, metadata, descriptionsArray, consents);
|
||||||
|
|
||||||
clearInterval(progressInterval);
|
clearInterval(progressInterval);
|
||||||
setUploadProgress(100);
|
setUploadProgress(100);
|
||||||
|
|
||||||
// Kurze Verzögerung für UX, dann Erfolgsmeldung anzeigen
|
// Show success dialog
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUploadComplete(true);
|
setUploadComplete(true);
|
||||||
setUploadResult(result);
|
setUploadResult(result);
|
||||||
|
setShowSuccessDialog(true);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -224,6 +247,12 @@ function MultiUploadPage() {
|
||||||
|
|
||||||
{selectedImages.length > 0 && (
|
{selectedImages.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
<ConsentCheckboxes
|
||||||
|
consents={consents}
|
||||||
|
onConsentChange={setConsents}
|
||||||
|
disabled={uploading}
|
||||||
|
/>
|
||||||
|
|
||||||
<DescriptionInput
|
<DescriptionInput
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
onMetadataChange={setMetadata}
|
onMetadataChange={setMetadata}
|
||||||
|
|
@ -251,7 +280,7 @@ function MultiUploadPage() {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={uploading || selectedImages.length === 0}
|
disabled={uploading || selectedImages.length === 0 || !consents.workshopConsent}
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
🚀 {selectedImages.length} Bild{selectedImages.length !== 1 ? 'er' : ''} hochladen
|
🚀 {selectedImages.length} Bild{selectedImages.length !== 1 ? 'er' : ''} hochladen
|
||||||
|
|
@ -294,62 +323,25 @@ function MultiUploadPage() {
|
||||||
totalFiles={selectedImages.length}
|
totalFiles={selectedImages.length}
|
||||||
isUploading={uploading}
|
isUploading={uploading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{uploadComplete && uploadResult && (
|
|
||||||
<Box sx={{
|
|
||||||
mt: 4,
|
|
||||||
p: 3,
|
|
||||||
borderRadius: '12px',
|
|
||||||
background: 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)',
|
|
||||||
color: 'white',
|
|
||||||
boxShadow: '0 4px 20px rgba(76, 175, 80, 0.4)',
|
|
||||||
animation: 'slideIn 0.5s ease-out',
|
|
||||||
'@keyframes slideIn': {
|
|
||||||
from: {
|
|
||||||
opacity: 0,
|
|
||||||
transform: 'translateY(-20px)'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
opacity: 1,
|
|
||||||
transform: 'translateY(0)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<Typography sx={{ fontSize: '28px', fontWeight: 'bold', mb: 1 }}>
|
|
||||||
✅ Upload erfolgreich!
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ fontSize: '18px', mb: 3 }}>
|
|
||||||
{uploadResult.imageCount} Bild{uploadResult.imageCount !== 1 ? 'er' : ''} wurden hochgeladen.
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
sx={{
|
|
||||||
background: 'white',
|
|
||||||
color: '#4CAF50',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: '16px',
|
|
||||||
px: 4,
|
|
||||||
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()}
|
|
||||||
>
|
|
||||||
👍 Alles klar!
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
{/* Success Dialog */}
|
||||||
|
{showSuccessDialog && uploadResult && (
|
||||||
|
<UploadSuccessDialog
|
||||||
|
open={showSuccessDialog}
|
||||||
|
onClose={() => {
|
||||||
|
setShowSuccessDialog(false);
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
groupId={uploadResult.groupId}
|
||||||
|
uploadCount={uploadResult.imageCount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="footerContainer">
|
<div className="footerContainer">
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Batch-Upload Funktion für mehrere Bilder
|
// Batch-Upload Funktion für mehrere Bilder
|
||||||
export const uploadImageBatch = async (images, metadata, descriptions = [], onProgress) => {
|
export const uploadImageBatch = async (images, metadata, descriptions = [], consents = null, onProgress) => {
|
||||||
if (!images || images.length === 0) {
|
if (!images || images.length === 0) {
|
||||||
throw new Error('Keine Bilder zum Upload ausgewählt');
|
throw new Error('Keine Bilder zum Upload ausgewählt');
|
||||||
}
|
}
|
||||||
|
|
@ -14,11 +14,16 @@ export const uploadImageBatch = async (images, metadata, descriptions = [], onPr
|
||||||
// Füge Metadaten hinzu
|
// Füge Metadaten hinzu
|
||||||
formData.append('metadata', JSON.stringify(metadata || {}));
|
formData.append('metadata', JSON.stringify(metadata || {}));
|
||||||
|
|
||||||
// Füge Beschreibungen hinzu (NEU)
|
// Füge Beschreibungen hinzu
|
||||||
if (descriptions && descriptions.length > 0) {
|
if (descriptions && descriptions.length > 0) {
|
||||||
formData.append('descriptions', JSON.stringify(descriptions));
|
formData.append('descriptions', JSON.stringify(descriptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Füge Einwilligungen hinzu (GDPR)
|
||||||
|
if (consents) {
|
||||||
|
formData.append('consents', JSON.stringify(consents));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/upload/batch', {
|
const response = await fetch('/api/upload/batch', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user