Project-Image-Uploader/frontend/src/Components/Pages/MultiUploadPage.js
matthias.lotz 5bc2b0d222 refactor(frontend): Improve consent and success UX
- Move ConsentCheckboxes below DescriptionInput for better flow
- Replace success dialog with inline success display
- Add copy-to-clipboard button for group ID
- Show detailed next steps and GDPR contact info inline
2025-11-09 21:49:33 +01:00

490 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { Button, Card, CardContent, Typography, Container, Box } from '@mui/material';
import Swal from 'sweetalert2/dist/sweetalert2.js';
import 'sweetalert2/src/sweetalert2.scss';
// Components
import Navbar from '../ComponentUtils/Headers/Navbar';
import Footer from '../ComponentUtils/Footer';
import MultiImageDropzone from '../ComponentUtils/MultiUpload/MultiImageDropzone';
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';
// Utils
import { uploadImageBatch } from '../../Utils/batchUpload';
// Styles
import '../../App.css';
// Background.css is now globally imported in src/index.js
// Styles migrated to MUI sx props in-place below
function MultiUploadPage() {
const [selectedImages, setSelectedImages] = useState([]);
const [metadata, setMetadata] = useState({
year: new Date().getFullYear(),
title: '',
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({});
// Cleanup object URLs when component unmounts
useEffect(() => {
return () => {
selectedImages.forEach(img => {
if (img.url && img.url.startsWith('blob:')) {
URL.revokeObjectURL(img.url);
}
});
};
}, [selectedImages]);
const handleImagesSelected = (newImages) => {
console.log('handleImagesSelected called with:', newImages);
// Convert File objects to preview objects with URLs
const imageObjects = newImages.map((file, index) => ({
id: `preview-${Date.now()}-${index}`, // Unique ID für Preview-Modus
file: file, // Original File object for upload
url: URL.createObjectURL(file), // Preview URL
name: file.name,
originalName: file.name,
size: file.size,
type: file.type
}));
setSelectedImages(prev => {
const updated = [...prev, ...imageObjects];
return updated;
});
};
const handleRemoveImage = (indexToRemove) => {
setSelectedImages(prev => {
const imageToRemove = prev[indexToRemove];
// Clean up the object URL to avoid memory leaks
if (imageToRemove && imageToRemove.url && imageToRemove.url.startsWith('blob:')) {
URL.revokeObjectURL(imageToRemove.url);
}
return prev.filter((_, index) => index !== indexToRemove);
});
};
const handleClearAll = () => {
// Clean up all object URLs
selectedImages.forEach(img => {
if (img.url && img.url.startsWith('blob:')) {
URL.revokeObjectURL(img.url);
}
});
setSelectedImages([]);
setMetadata({
year: new Date().getFullYear(),
title: '',
description: '',
name: ''
});
setConsents({
workshopConsent: false,
socialMediaConsents: []
});
setImageDescriptions({});
setIsEditMode(false);
};
// Handle drag-and-drop reordering (only updates local state, no API call)
const handleReorder = (reorderedItems) => {
console.log('Reordering images in preview:', reorderedItems);
setSelectedImages(reorderedItems);
};
// Handle edit mode toggle
const handleEditMode = (enabled) => {
setIsEditMode(enabled);
};
// Handle description changes
const handleDescriptionChange = (imageId, description) => {
setImageDescriptions(prev => ({
...prev,
[imageId]: description.slice(0, 200) // Enforce max length
}));
};
const handleUpload = async () => {
if (selectedImages.length === 0) {
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()) {
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) {
Swal.fire({
icon: 'error',
title: 'Einwilligung erforderlich',
text: 'Die Zustimmung zur Anzeige in der Werkstatt ist erforderlich.',
confirmButtonColor: '#f44336'
});
return;
}
setUploading(true);
setUploadProgress(0);
try {
// Simuliere Progress (da wir noch keinen echten Progress haben)
const progressInterval = setInterval(() => {
setUploadProgress(prev => {
if (prev >= 90) {
clearInterval(progressInterval);
return 90;
}
return prev + 10;
});
}, 200);
// Extract the actual File objects from our image objects
const filesToUpload = selectedImages.map(img => img.file || img);
// 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);
setUploadResult(result);
}, 500);
} catch (error) {
setUploading(false);
console.error('Upload error:', error);
Swal.fire({
icon: 'error',
title: 'Upload fehlgeschlagen',
text: error.message || 'Ein Fehler ist beim Upload aufgetreten.',
confirmButtonColor: '#f44336'
});
}
};
return (
<div className="allContainer">
<Navbar />
<Container maxWidth="lg" sx={{ pt: '20px', pb: '40px', minHeight: '80vh' }}>
<Card sx={{ borderRadius: '12px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', p: '20px', mb: '20px' }}>
<CardContent>
<Typography sx={{ fontFamily: 'roboto', fontWeight: 400, fontSize: '28px', textAlign: 'center', mb: '10px', color: '#333333' }}>
Project Image Uploader
</Typography>
<Typography sx={{ fontFamily: 'roboto', fontWeight: 300, fontSize: '16px', color: '#666666', textAlign: 'center', mb: '30px' }}>
Lade ein oder mehrere Bilder von deinem Projekt hoch und beschreibe dein Projekt in wenigen Worten.
<br />
Die Bilder werden nur hier im Hobbyhimmel auf dem Monitor gezeigt, es wird an keine Dritten weiter gegeben.
</Typography>
{!uploading ? (
<>
<MultiImageDropzone
onImagesSelected={handleImagesSelected}
selectedImages={selectedImages}
/>
<ImageGallery
items={selectedImages}
onDelete={handleRemoveImage}
mode="preview"
showActions={true}
enableReordering={true}
onReorder={handleReorder}
isEditMode={isEditMode}
onEditMode={handleEditMode}
imageDescriptions={imageDescriptions}
onDescriptionChange={handleDescriptionChange}
/>
{selectedImages.length > 0 && (
<>
<DescriptionInput
metadata={metadata}
onMetadataChange={setMetadata}
/>
<ConsentCheckboxes
consents={consents}
onConsentChange={setConsents}
disabled={uploading}
/>
<Box sx={{ display: 'flex', gap: '15px', justifyContent: 'center', mt: '20px', flexWrap: 'wrap' }}>
<Button
sx={{
borderRadius: '25px',
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}
disabled={uploading || selectedImages.length === 0 || !consents.workshopConsent}
size="large"
>
🚀 {selectedImages.length} Bild{selectedImages.length !== 1 ? 'er' : ''} hochladen
</Button>
<Button
sx={{
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}
size="large"
>
🗑 Alle entfernen
</Button>
</Box>
</>
)}
</>
) : (
<div style={{ textAlign: 'center', padding: '40px 0' }}>
{!uploadComplete ? (
<>
<Loading />
<UploadProgress
progress={uploadProgress}
totalFiles={selectedImages.length}
isUploading={uploading}
/>
</>
) : (
<Box sx={{
mt: 4,
p: 4,
borderRadius: '16px',
background: 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)',
color: 'white',
boxShadow: '0 8px 32px 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)'
}
}
}}>
{/* Success Icon & Title */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 2, mb: 2 }}>
<Box sx={{
bgcolor: 'white',
borderRadius: '50%',
width: 60,
height: 60,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 4px 12px rgba(0,0,0,0.2)'
}}>
<Typography sx={{ fontSize: '32px' }}></Typography>
</Box>
<Typography sx={{ fontSize: '32px', fontWeight: 'bold' }}>
Upload erfolgreich!
</Typography>
</Box>
{/* Upload Count */}
<Typography sx={{ fontSize: '18px', mb: 3 }}>
<strong>{uploadResult?.imageCount || 0}</strong> {uploadResult?.imageCount === 1 ? 'Bild wurde' : 'Bilder wurden'} erfolgreich hochgeladen
und {uploadResult?.imageCount === 1 ? 'wird' : 'werden'} nach der Prüfung durch das Hobbyhimmel-Team angezeigt.
</Typography>
{/* Group ID Box */}
<Box sx={{
bgcolor: 'rgba(255,255,255,0.2)',
borderRadius: '12px',
p: 3,
mb: 3,
border: '2px solid rgba(255,255,255,0.3)'
}}>
<Typography sx={{ fontSize: '14px', mb: 1, opacity: 0.9 }}>
Ihre Referenz-Nummer:
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 2 }}>
<Typography sx={{
fontSize: '24px',
fontFamily: 'monospace',
fontWeight: 'bold',
letterSpacing: '2px'
}}>
{uploadResult?.groupId}
</Typography>
<Button
sx={{
bgcolor: 'white',
color: '#4CAF50',
minWidth: 'auto',
px: 2,
py: 1,
'&:hover': {
bgcolor: '#f0f0f0'
}
}}
onClick={() => {
navigator.clipboard.writeText(uploadResult?.groupId || '');
}}
>
📋 Kopieren
</Button>
</Box>
<Typography sx={{ fontSize: '12px', mt: 1, opacity: 0.8 }}>
Notieren Sie sich diese Nummer für spätere Anfragen
</Typography>
</Box>
{/* Next Steps */}
<Box sx={{
bgcolor: 'rgba(255,255,255,0.15)',
borderRadius: '8px',
p: 2,
mb: 3,
textAlign: 'left'
}}>
<Typography sx={{ fontSize: '16px', fontWeight: 600, mb: 1.5 }}>
Was passiert jetzt?
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<Typography sx={{ fontSize: '14px' }}></Typography>
<Typography sx={{ fontSize: '14px' }}>
Ihre Bilder werden vom Team geprüft
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Typography sx={{ fontSize: '14px' }}></Typography>
<Typography sx={{ fontSize: '14px' }}>
Nach Freigabe erscheinen sie auf dem Werkstatt-Monitor
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Typography sx={{ fontSize: '14px' }}></Typography>
<Typography sx={{ fontSize: '14px' }}>
Bei gewählter Social Media Einwilligung werden sie entsprechend veröffentlicht
</Typography>
</Box>
</Box>
</Box>
{/* GDPR Contact */}
<Typography sx={{ fontSize: '12px', opacity: 0.9, mb: 3 }}>
<strong>Fragen oder Widerruf?</strong> Kontaktieren Sie uns mit Ihrer Referenz-Nummer unter: <strong>it@hobbyhimmel.de</strong>
</Typography>
{/* Action Button */}
<Button
sx={{
bgcolor: 'white',
color: '#4CAF50',
fontWeight: 'bold',
fontSize: '16px',
px: 5,
py: 1.5,
borderRadius: '25px',
textTransform: 'none',
boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
'&:hover': {
bgcolor: '#f0f0f0',
transform: 'scale(1.05)',
boxShadow: '0 6px 16px rgba(0,0,0,0.3)'
},
transition: 'all 0.3s ease'
}}
onClick={() => window.location.reload()}
>
👍 Weitere Bilder hochladen
</Button>
</Box>
)}
</div>
)}
</CardContent>
</Card>
</Container>
<div className="footerContainer">
<Footer />
</div>
</div>
);
}
export default MultiUploadPage;