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:
sebseb7
2025-07-18 11:56:37 +02:00
parent e43b894bfc
commit 47a882b667
5 changed files with 1170 additions and 31 deletions

View 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;