From 39f133eadf0ec1e796e62bcdd20c6872bdef5575 Mon Sep 17 00:00:00 2001 From: "matthias.lotz" Date: Sun, 9 Nov 2025 21:11:01 +0100 Subject: [PATCH] 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 --- .../MultiUpload/ConsentCheckboxes.js | 206 ++++++++++++++++++ .../MultiUpload/UploadSuccessDialog.js | 188 ++++++++++++++++ .../src/Components/Pages/MultiUploadPage.js | 98 ++++----- frontend/src/Utils/batchUpload.js | 9 +- 4 files changed, 446 insertions(+), 55 deletions(-) create mode 100644 frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js create mode 100644 frontend/src/Components/ComponentUtils/MultiUpload/UploadSuccessDialog.js diff --git a/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js b/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js new file mode 100644 index 0000000..9b30d41 --- /dev/null +++ b/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js @@ -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 ( + + {/* Aufklärungshinweis */} + } sx={{ mb: 3 }}> + + Wichtiger Hinweis + + + 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. + + + Nach dem Upload erhalten Sie eine Gruppen-ID als Referenz für die Kontaktaufnahme. + + + + {/* Pflicht-Zustimmung: Werkstatt-Anzeige */} + + + Anzeige in der Werkstatt * + + + } + label={ + + 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. (Pflichtfeld) + + } + /> + + + + + {/* Optional: Social Media Veröffentlichung */} + + + Social Media Veröffentlichung (optional) + + + 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): + + + {loading ? ( + + Lade Plattformen... + + ) : error ? ( + + {error} + + ) : ( + + {platforms.map(platform => { + const IconComponent = ICON_MAP[platform.icon_name] || InfoIcon; + return ( + + } + label={ + + + + {platform.display_name} + + + } + /> + ); + })} + + )} + + + {/* Widerrufs-Hinweis */} + + + Widerruf Ihrer Einwilligung: Sie können Ihre Einwilligung + jederzeit widerrufen. Kontaktieren Sie uns hierzu mit Ihrer Gruppen-ID unter:{' '} + it@hobbyhimmel.de + + + + ); +} + +export default ConsentCheckboxes; diff --git a/frontend/src/Components/ComponentUtils/MultiUpload/UploadSuccessDialog.js b/frontend/src/Components/ComponentUtils/MultiUpload/UploadSuccessDialog.js new file mode 100644 index 0000000..5540e55 --- /dev/null +++ b/frontend/src/Components/ComponentUtils/MultiUpload/UploadSuccessDialog.js @@ -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 ( + + {/* Header mit Schließen-Button */} + + + + + + Upload erfolgreich! + + + + + + + + + + {/* Success Message */} + + + {uploadCount} {uploadCount === 1 ? 'Bild wurde' : 'Bilder wurden'} erfolgreich hochgeladen + und werden nach der Prüfung durch das Hobbyhimmel-Team angezeigt. + + + + {/* Gruppen-ID Anzeige */} + + + Ihre Referenz-Nummer: + + + + {groupId} + + + + + + + + + Notieren Sie sich diese Nummer für spätere Anfragen an das Hobbyhimmel-Team. + + + + + + {/* Nächste Schritte */} + + + Was passiert jetzt? + + + + + + Ihre Bilder werden vom Team geprüft + + + + + + Nach Freigabe erscheinen sie auf dem Werkstatt-Monitor + + + + + + Bei gewählter Social Media Einwilligung werden sie entsprechend veröffentlicht + + + + + + {/* GDPR Kontakt-Info */} + + + Fragen oder Widerruf Ihrer Einwilligung? + + + Kontaktieren Sie uns mit Ihrer Referenz-Nummer unter:{' '} + it@hobbyhimmel.de + + + + + + + + + ); +} + +export default UploadSuccessDialog; diff --git a/frontend/src/Components/Pages/MultiUploadPage.js b/frontend/src/Components/Pages/MultiUploadPage.js index 12728d8..87859aa 100644 --- a/frontend/src/Components/Pages/MultiUploadPage.js +++ b/frontend/src/Components/Pages/MultiUploadPage.js @@ -11,6 +11,8 @@ import ImageGallery from '../ComponentUtils/ImageGallery'; import DescriptionInput from '../ComponentUtils/MultiUpload/DescriptionInput'; import UploadProgress from '../ComponentUtils/MultiUpload/UploadProgress'; import Loading from '../ComponentUtils/LoadingAnimation/Loading'; +import ConsentCheckboxes from '../ComponentUtils/MultiUpload/ConsentCheckboxes'; +import UploadSuccessDialog from '../ComponentUtils/MultiUpload/UploadSuccessDialog'; // Utils import { uploadImageBatch } from '../../Utils/batchUpload'; @@ -30,12 +32,17 @@ function MultiUploadPage() { description: '', name: '' }); + const [consents, setConsents] = useState({ + workshopConsent: false, + socialMediaConsents: [] + }); const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadComplete, setUploadComplete] = useState(false); const [uploadResult, setUploadResult] = useState(null); const [isEditMode, setIsEditMode] = useState(false); const [imageDescriptions, setImageDescriptions] = useState({}); + const [showSuccessDialog, setShowSuccessDialog] = useState(false); // Cleanup object URLs when component unmounts useEffect(() => { @@ -94,6 +101,10 @@ function MultiUploadPage() { description: '', name: '' }); + setConsents({ + workshopConsent: false, + socialMediaConsents: [] + }); setImageDescriptions({}); setIsEditMode(false); }; @@ -138,6 +149,17 @@ function MultiUploadPage() { 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); setUploadProgress(0); @@ -162,15 +184,16 @@ function MultiUploadPage() { description: imageDescriptions[img.id] || '' })); - const result = await uploadImageBatch(filesToUpload, metadata, descriptionsArray); + const result = await uploadImageBatch(filesToUpload, metadata, descriptionsArray, consents); clearInterval(progressInterval); setUploadProgress(100); - // Kurze Verzögerung für UX, dann Erfolgsmeldung anzeigen + // Show success dialog setTimeout(() => { setUploadComplete(true); setUploadResult(result); + setShowSuccessDialog(true); }, 500); } catch (error) { @@ -224,6 +247,12 @@ function MultiUploadPage() { {selectedImages.length > 0 && ( <> + + 🚀 {selectedImages.length} Bild{selectedImages.length !== 1 ? 'er' : ''} hochladen @@ -294,62 +323,25 @@ function MultiUploadPage() { totalFiles={selectedImages.length} isUploading={uploading} /> - - {uploadComplete && uploadResult && ( - - - ✅ Upload erfolgreich! - - - {uploadResult.imageCount} Bild{uploadResult.imageCount !== 1 ? 'er' : ''} wurden hochgeladen. - - - - )} )} + {/* Success Dialog */} + {showSuccessDialog && uploadResult && ( + { + setShowSuccessDialog(false); + window.location.reload(); + }} + groupId={uploadResult.groupId} + uploadCount={uploadResult.imageCount} + /> + )} +
diff --git a/frontend/src/Utils/batchUpload.js b/frontend/src/Utils/batchUpload.js index 0ad93a7..d7e6bab 100644 --- a/frontend/src/Utils/batchUpload.js +++ b/frontend/src/Utils/batchUpload.js @@ -1,5 +1,5 @@ // 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) { 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 formData.append('metadata', JSON.stringify(metadata || {})); - // Füge Beschreibungen hinzu (NEU) + // Füge Beschreibungen hinzu if (descriptions && descriptions.length > 0) { formData.append('descriptions', JSON.stringify(descriptions)); } + // Füge Einwilligungen hinzu (GDPR) + if (consents) { + formData.append('consents', JSON.stringify(consents)); + } + try { const response = await fetch('/api/upload/batch', { method: 'POST',