- 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
490 lines
18 KiB
JavaScript
490 lines
18 KiB
JavaScript
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; |