Add article interaction forms: Implemented ArticleAvailabilityForm, ArticleQuestionForm, and ArticleRatingForm components for user inquiries, ratings, and availability requests. Integrated photo upload functionality and enhanced user experience with collapsible sections in ProductDetailPage for better interaction with product details.
This commit is contained in:
272
src/components/PhotoUpload.js
Normal file
272
src/components/PhotoUpload.js
Normal file
@@ -0,0 +1,272 @@
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user