286 lines
7.8 KiB
JavaScript
286 lines
7.8 KiB
JavaScript
import React, { Component } from 'react';
|
|
import {
|
|
Box,
|
|
Button,
|
|
Typography,
|
|
IconButton,
|
|
Paper,
|
|
Grid,
|
|
Alert
|
|
} from '@mui/material';
|
|
import {
|
|
Delete,
|
|
CloudUpload
|
|
} from '@mui/icons-material';
|
|
|
|
class PhotoUpload extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
files: [],
|
|
previews: [],
|
|
error: null
|
|
};
|
|
this.fileInputRef = React.createRef();
|
|
}
|
|
|
|
handleFileSelect = (event) => {
|
|
const selectedFiles = Array.from(event.target.files);
|
|
const maxFiles = this.props.maxFiles || 5;
|
|
const maxSize = this.props.maxSize || 50 * 1024 * 1024; // 50MB default - will be compressed
|
|
|
|
// Validate file count
|
|
if (this.state.files.length + selectedFiles.length > maxFiles) {
|
|
this.setState({
|
|
error: `Maximal ${maxFiles} Dateien erlaubt`
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Validate file types and sizes
|
|
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
const validFiles = [];
|
|
const newPreviews = [];
|
|
|
|
for (const file of selectedFiles) {
|
|
if (!validTypes.includes(file.type)) {
|
|
this.setState({
|
|
error: 'Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt'
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (file.size > maxSize) {
|
|
this.setState({
|
|
error: `Datei zu groß. Maximum: ${Math.round(maxSize / (1024 * 1024))}MB`
|
|
});
|
|
continue;
|
|
}
|
|
|
|
validFiles.push(file);
|
|
|
|
// Create preview and compress image
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
// Compress the image
|
|
this.compressImage(e.target.result, file.name, (compressedFile) => {
|
|
newPreviews.push({
|
|
file: compressedFile,
|
|
preview: e.target.result,
|
|
name: file.name,
|
|
originalSize: file.size,
|
|
compressedSize: compressedFile.size
|
|
});
|
|
|
|
if (newPreviews.length === validFiles.length) {
|
|
const compressedFiles = newPreviews.map(p => p.file);
|
|
this.setState(prevState => ({
|
|
files: [...prevState.files, ...compressedFiles],
|
|
previews: [...prevState.previews, ...newPreviews],
|
|
error: null
|
|
}), () => {
|
|
// Notify parent component
|
|
if (this.props.onChange) {
|
|
this.props.onChange(this.state.files);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
// Reset input
|
|
event.target.value = '';
|
|
};
|
|
|
|
handleRemoveFile = (index) => {
|
|
this.setState(prevState => {
|
|
const newFiles = prevState.files.filter((_, i) => i !== index);
|
|
const newPreviews = prevState.previews.filter((_, i) => i !== index);
|
|
|
|
// Notify parent component
|
|
if (this.props.onChange) {
|
|
this.props.onChange(newFiles);
|
|
}
|
|
|
|
return {
|
|
files: newFiles,
|
|
previews: newPreviews
|
|
};
|
|
});
|
|
};
|
|
|
|
compressImage = (dataURL, fileName, callback) => {
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const img = new Image();
|
|
|
|
img.onload = () => {
|
|
// Calculate new dimensions (max 1920x1080 for submission)
|
|
const maxWidth = 1920;
|
|
const maxHeight = 1080;
|
|
let { width, height } = img;
|
|
|
|
if (width > height) {
|
|
if (width > maxWidth) {
|
|
height = (height * maxWidth) / width;
|
|
width = maxWidth;
|
|
}
|
|
} else {
|
|
if (height > maxHeight) {
|
|
width = (width * maxHeight) / height;
|
|
height = maxHeight;
|
|
}
|
|
}
|
|
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
// Draw and compress
|
|
ctx.drawImage(img, 0, 0, width, height);
|
|
|
|
// Convert to blob with compression
|
|
canvas.toBlob((blob) => {
|
|
const compressedFile = new File([blob], fileName, {
|
|
type: 'image/jpeg',
|
|
lastModified: Date.now()
|
|
});
|
|
callback(compressedFile);
|
|
}, 'image/jpeg', 0.8); // 80% quality
|
|
};
|
|
|
|
img.src = dataURL;
|
|
};
|
|
|
|
// Method to reset the component
|
|
reset = () => {
|
|
this.setState({
|
|
files: [],
|
|
previews: [],
|
|
error: null
|
|
});
|
|
|
|
// Also reset the file input
|
|
if (this.fileInputRef.current) {
|
|
this.fileInputRef.current.value = '';
|
|
}
|
|
};
|
|
|
|
render() {
|
|
const { files, previews, error } = this.state;
|
|
const { disabled, label } = this.props;
|
|
|
|
return (
|
|
<Box>
|
|
<Typography variant="body2" sx={{ mb: 1, fontWeight: 500 }}>
|
|
{label || 'Fotos anhängen (optional)'}
|
|
</Typography>
|
|
|
|
<input
|
|
ref={this.fileInputRef}
|
|
type="file"
|
|
multiple
|
|
accept="image/*"
|
|
style={{ display: 'none' }}
|
|
onChange={this.handleFileSelect}
|
|
disabled={disabled}
|
|
/>
|
|
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<CloudUpload />}
|
|
onClick={() => this.fileInputRef.current?.click()}
|
|
disabled={disabled}
|
|
sx={{ mb: 2 }}
|
|
>
|
|
Fotos auswählen
|
|
</Button>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{previews.length > 0 && (
|
|
<Grid container spacing={2}>
|
|
{previews.map((preview, index) => (
|
|
<Grid item xs={6} sm={4} md={3} key={index}>
|
|
<Paper
|
|
sx={{
|
|
position: 'relative',
|
|
p: 1,
|
|
borderRadius: 1,
|
|
overflow: 'hidden'
|
|
}}
|
|
>
|
|
<Box
|
|
component="img"
|
|
src={preview.preview}
|
|
alt={preview.name}
|
|
sx={{
|
|
width: '100%',
|
|
height: '100px',
|
|
objectFit: 'cover',
|
|
borderRadius: 1
|
|
}}
|
|
/>
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => this.handleRemoveFile(index)}
|
|
disabled={disabled}
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 4,
|
|
right: 4,
|
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
color: 'white',
|
|
'&:hover': {
|
|
backgroundColor: 'rgba(0,0,0,0.9)'
|
|
}
|
|
}}
|
|
>
|
|
<Delete fontSize="small" />
|
|
</IconButton>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{
|
|
position: 'absolute',
|
|
bottom: 4,
|
|
left: 4,
|
|
right: 4,
|
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
color: 'white',
|
|
p: 0.5,
|
|
borderRadius: 0.5,
|
|
fontSize: '0.7rem',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap'
|
|
}}
|
|
>
|
|
{preview.name}
|
|
</Typography>
|
|
</Paper>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
)}
|
|
|
|
{files.length > 0 && (
|
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
|
{files.length} Datei(en) ausgewählt
|
|
{previews.length > 0 && previews.some(p => p.originalSize && p.compressedSize) && (
|
|
<span style={{ marginLeft: '8px' }}>
|
|
(komprimiert für Upload)
|
|
</span>
|
|
)}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default PhotoUpload;
|