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,242 @@
import React, { Component } from 'react';
import {
Box,
Typography,
TextField,
Button,
Paper,
Alert,
CircularProgress,
FormControl,
FormLabel,
RadioGroup,
FormControlLabel,
Radio
} from '@mui/material';
import { withI18n } from '../i18n/withTranslation.js';
class ArticleAvailabilityForm extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
telegramId: '',
notificationMethod: 'email',
message: '',
loading: false,
success: false,
error: null
};
}
handleInputChange = (field) => (event) => {
this.setState({ [field]: event.target.value });
};
handleNotificationMethodChange = (event) => {
this.setState({
notificationMethod: event.target.value,
// Clear the other field when switching methods
email: event.target.value === 'email' ? this.state.email : '',
telegramId: event.target.value === 'telegram' ? this.state.telegramId : ''
});
};
handleSubmit = (event) => {
event.preventDefault();
// Prepare data for API emission
const availabilityData = {
type: 'availability_inquiry',
productId: this.props.productId,
productName: this.props.productName,
name: this.state.name,
notificationMethod: this.state.notificationMethod,
email: this.state.notificationMethod === 'email' ? this.state.email : '',
telegramId: this.state.notificationMethod === 'telegram' ? this.state.telegramId : '',
message: this.state.message,
timestamp: new Date().toISOString()
};
// Emit data via socket
console.log('Availability Inquiry Data to emit:', availabilityData);
if (this.props.socket) {
this.props.socket.emit('availability_inquiry_submit', availabilityData);
// Set up response handler
this.props.socket.once('availability_inquiry_response', (response) => {
if (response.success) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
telegramId: '',
notificationMethod: 'email',
message: ''
});
} else {
this.setState({
loading: false,
error: response.error || 'Ein Fehler ist aufgetreten'
});
}
// Clear messages after 3 seconds
setTimeout(() => {
this.setState({ success: false, error: null });
}, 3000);
});
}
this.setState({ loading: true });
// Fallback timeout in case backend doesn't respond
setTimeout(() => {
if (this.state.loading) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
telegramId: '',
notificationMethod: 'email',
message: ''
});
// Clear success message after 3 seconds
setTimeout(() => {
this.setState({ success: false });
}, 3000);
}
}, 5000);
};
render() {
const { name, email, telegramId, notificationMethod, message, loading, success, error } = this.state;
return (
<Paper id="availability-form" sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Verfügbarkeit anfragen
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Dieser Artikel ist derzeit nicht verfügbar. Gerne informieren wir Sie, sobald er wieder lieferbar ist.
</Typography>
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Anfrage! Wir werden Sie {notificationMethod === 'email' ? 'per E-Mail' : 'über Telegram'} informieren, sobald der Artikel wieder verfügbar ist.
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Name"
value={name}
onChange={this.handleInputChange('name')}
required
fullWidth
disabled={loading}
placeholder="Ihr Name"
/>
<FormControl component="fieldset" disabled={loading}>
<FormLabel component="legend" sx={{ mb: 1 }}>
Wie möchten Sie benachrichtigt werden?
</FormLabel>
<RadioGroup
value={notificationMethod}
onChange={this.handleNotificationMethodChange}
row
>
<FormControlLabel
value="email"
control={<Radio />}
label="E-Mail"
/>
<FormControlLabel
value="telegram"
control={<Radio />}
label="Telegram Bot"
/>
</RadioGroup>
</FormControl>
{notificationMethod === 'email' && (
<TextField
label="E-Mail"
type="email"
value={email}
onChange={this.handleInputChange('email')}
required
fullWidth
disabled={loading}
placeholder="ihre.email@example.com"
/>
)}
{notificationMethod === 'telegram' && (
<TextField
label="Telegram ID"
value={telegramId}
onChange={this.handleInputChange('telegramId')}
required
fullWidth
disabled={loading}
placeholder="@IhrTelegramName oder Telegram ID"
helperText="Geben Sie Ihren Telegram-Benutzernamen (mit @) oder Ihre Telegram-ID ein"
/>
)}
<TextField
label="Nachricht (optional)"
value={message}
onChange={this.handleInputChange('message')}
fullWidth
multiline
rows={3}
disabled={loading}
placeholder="Zusätzliche Informationen oder Fragen..."
/>
<Button
type="submit"
variant="contained"
disabled={loading || !name || (notificationMethod === 'email' && !email) || (notificationMethod === 'telegram' && !telegramId)}
sx={{
mt: 2,
py: 1.5,
fontSize: '1rem',
fontWeight: 600,
backgroundColor: 'warning.main',
'&:hover': {
backgroundColor: 'warning.dark'
}
}}
>
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet...
</>
) : (
'Verfügbarkeit anfragen'
)}
</Button>
</Box>
</Paper>
);
}
}
export default withI18n()(ArticleAvailabilityForm);

View File

@@ -0,0 +1,225 @@
import React, { Component } from 'react';
import {
Box,
Typography,
TextField,
Button,
Paper,
Alert,
CircularProgress
} from '@mui/material';
import { withI18n } from '../i18n/withTranslation.js';
import PhotoUpload from './PhotoUpload.js';
class ArticleQuestionForm extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
question: '',
photos: [],
loading: false,
success: false,
error: null
};
}
handleInputChange = (field) => (event) => {
this.setState({ [field]: event.target.value });
};
handlePhotosChange = (files) => {
this.setState({ photos: files });
};
convertPhotosToBase64 = (photos) => {
return Promise.all(
photos.map(photo => {
return new Promise((resolve, _reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve({
name: photo.name,
type: photo.type,
size: photo.size,
data: e.target.result // base64 string
});
};
reader.readAsDataURL(photo);
});
})
);
};
handleSubmit = async (event) => {
event.preventDefault();
this.setState({ loading: true });
try {
// Convert photos to base64
const photosBase64 = await this.convertPhotosToBase64(this.state.photos);
// Prepare data for API emission
const questionData = {
type: 'article_question',
productId: this.props.productId,
productName: this.props.productName,
name: this.state.name,
email: this.state.email,
question: this.state.question,
photos: photosBase64,
timestamp: new Date().toISOString()
};
// Emit data via socket
console.log('Article Question Data to emit:', questionData);
if (this.props.socket) {
this.props.socket.emit('article_question_submit', questionData);
// Set up response handler
this.props.socket.once('article_question_response', (response) => {
if (response.success) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
question: '',
photos: []
});
} else {
this.setState({
loading: false,
error: response.error || 'Ein Fehler ist aufgetreten'
});
}
// Clear messages after 3 seconds
setTimeout(() => {
this.setState({ success: false, error: null });
}, 3000);
});
}
} catch {
this.setState({
loading: false,
error: 'Fehler beim Verarbeiten der Fotos'
});
}
// Fallback timeout in case backend doesn't respond
setTimeout(() => {
if (this.state.loading) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
question: '',
photos: []
});
// Clear success message after 3 seconds
setTimeout(() => {
this.setState({ success: false });
}, 3000);
}
}, 5000);
};
render() {
const { name, email, question, loading, success, error } = this.state;
return (
<Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Frage zum Artikel
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Haben Sie eine Frage zu diesem Artikel? Wir helfen Ihnen gerne weiter.
</Typography>
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Frage! Wir werden uns schnellstmöglich bei Ihnen melden.
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Name"
value={name}
onChange={this.handleInputChange('name')}
required
fullWidth
disabled={loading}
placeholder="Ihr Name"
/>
<TextField
label="E-Mail"
type="email"
value={email}
onChange={this.handleInputChange('email')}
required
fullWidth
disabled={loading}
placeholder="ihre.email@example.com"
/>
<TextField
label="Ihre Frage"
value={question}
onChange={this.handleInputChange('question')}
required
fullWidth
multiline
rows={4}
disabled={loading}
placeholder="Beschreiben Sie Ihre Frage zu diesem Artikel..."
/>
<PhotoUpload
onChange={this.handlePhotosChange}
disabled={loading}
maxFiles={3}
label="Fotos zur Frage anhängen (optional)"
/>
<Button
type="submit"
variant="contained"
disabled={loading || !name || !email || !question}
sx={{
mt: 2,
py: 1.5,
fontSize: '1rem',
fontWeight: 600
}}
>
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet...
</>
) : (
'Frage senden'
)}
</Button>
</Box>
</Paper>
);
}
}
export default withI18n()(ArticleQuestionForm);

View File

@@ -0,0 +1,254 @@
import React, { Component } from 'react';
import {
Box,
Typography,
TextField,
Button,
Paper,
Alert,
CircularProgress,
Rating
} from '@mui/material';
import StarIcon from '@mui/icons-material/Star';
import { withI18n } from '../i18n/withTranslation.js';
import PhotoUpload from './PhotoUpload.js';
class ArticleRatingForm extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
rating: 0,
review: '',
photos: [],
loading: false,
success: false,
error: null
};
}
handleInputChange = (field) => (event) => {
this.setState({ [field]: event.target.value });
};
handleRatingChange = (event, newValue) => {
this.setState({ rating: newValue });
};
handlePhotosChange = (files) => {
this.setState({ photos: files });
};
convertPhotosToBase64 = (photos) => {
return Promise.all(
photos.map(photo => {
return new Promise((resolve, _reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve({
name: photo.name,
type: photo.type,
size: photo.size,
data: e.target.result // base64 string
});
};
reader.readAsDataURL(photo);
});
})
);
};
handleSubmit = async (event) => {
event.preventDefault();
this.setState({ loading: true });
try {
// Convert photos to base64
const photosBase64 = await this.convertPhotosToBase64(this.state.photos);
// Prepare data for API emission
const ratingData = {
type: 'article_rating',
productId: this.props.productId,
productName: this.props.productName,
name: this.state.name,
email: this.state.email,
rating: this.state.rating,
review: this.state.review,
photos: photosBase64,
timestamp: new Date().toISOString()
};
// Emit data via socket
console.log('Article Rating Data to emit:', ratingData);
if (this.props.socket) {
this.props.socket.emit('article_rating_submit', ratingData);
// Set up response handler
this.props.socket.once('article_rating_response', (response) => {
if (response.success) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
rating: 0,
review: '',
photos: []
});
} else {
this.setState({
loading: false,
error: response.error || 'Ein Fehler ist aufgetreten'
});
}
// Clear messages after 3 seconds
setTimeout(() => {
this.setState({ success: false, error: null });
}, 3000);
});
}
} catch {
this.setState({
loading: false,
error: 'Fehler beim Verarbeiten der Fotos'
});
}
// Fallback timeout in case backend doesn't respond
setTimeout(() => {
if (this.state.loading) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
rating: 0,
review: '',
photos: []
});
// Clear success message after 3 seconds
setTimeout(() => {
this.setState({ success: false });
}, 3000);
}
}, 5000);
};
render() {
const { name, email, rating, review, loading, success, error } = this.state;
return (
<Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Artikel Bewerten
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Teilen Sie Ihre Erfahrungen mit diesem Artikel und helfen Sie anderen Kunden bei der Entscheidung.
</Typography>
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Bewertung! Sie wird nach Prüfung veröffentlicht.
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Name"
value={name}
onChange={this.handleInputChange('name')}
required
fullWidth
disabled={loading}
placeholder="Ihr Name"
/>
<TextField
label="E-Mail"
type="email"
value={email}
onChange={this.handleInputChange('email')}
required
fullWidth
disabled={loading}
placeholder="ihre.email@example.com"
helperText="Ihre E-Mail wird nicht veröffentlicht"
/>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
Bewertung *
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Rating
name="article-rating"
value={rating}
onChange={this.handleRatingChange}
size="large"
disabled={loading}
emptyIcon={<StarIcon style={{ opacity: 0.55 }} fontSize="inherit" />}
/>
<Typography variant="body2" color="text.secondary">
{rating > 0 ? `${rating} von 5 Sternen` : 'Bitte bewerten'}
</Typography>
</Box>
</Box>
<TextField
label="Ihre Bewertung (optional)"
value={review}
onChange={this.handleInputChange('review')}
fullWidth
multiline
rows={4}
disabled={loading}
placeholder="Beschreiben Sie Ihre Erfahrungen mit diesem Artikel..."
/>
<PhotoUpload
onChange={this.handlePhotosChange}
disabled={loading}
maxFiles={5}
label="Fotos zur Bewertung anhängen (optional)"
/>
<Button
type="submit"
variant="contained"
disabled={loading || !name || !email || rating === 0}
sx={{
mt: 2,
py: 1.5,
fontSize: '1rem',
fontWeight: 600
}}
>
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet...
</>
) : (
'Bewertung abgeben'
)}
</Button>
</Box>
</Paper>
);
}
}
export default withI18n()(ArticleRatingForm);

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;

View File

@@ -1,10 +1,13 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { Box, Typography, CardMedia, Stack, Chip } from "@mui/material"; import { Box, Typography, CardMedia, Stack, Chip, Button, Collapse } from "@mui/material";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import parse from "html-react-parser"; import parse from "html-react-parser";
import AddToCartButton from "./AddToCartButton.js"; import AddToCartButton from "./AddToCartButton.js";
import Images from "./Images.js"; import Images from "./Images.js";
import { withI18n } from "../i18n/withTranslation.js"; import { withI18n } from "../i18n/withTranslation.js";
import ArticleQuestionForm from "./ArticleQuestionForm.js";
import ArticleRatingForm from "./ArticleRatingForm.js";
import ArticleAvailabilityForm from "./ArticleAvailabilityForm.js";
// Utility function to clean product names by removing trailing number in parentheses // Utility function to clean product names by removing trailing number in parentheses
const cleanProductName = (name) => { const cleanProductName = (name) => {
@@ -35,7 +38,11 @@ class ProductDetailPage extends Component {
komponentenData: {}, // Store individual komponent data with loading states komponentenData: {}, // Store individual komponent data with loading states
komponentenImages: {}, // Store tiny pictures for komponenten komponentenImages: {}, // Store tiny pictures for komponenten
totalKomponentenPrice: 0, totalKomponentenPrice: 0,
totalSavings: 0 totalSavings: 0,
// Collapsible sections state
showQuestionForm: false,
showRatingForm: false,
showAvailabilityForm: false
}; };
} else { } else {
this.state = { this.state = {
@@ -51,7 +58,11 @@ class ProductDetailPage extends Component {
komponentenData: {}, // Store individual komponent data with loading states komponentenData: {}, // Store individual komponent data with loading states
komponentenImages: {}, // Store tiny pictures for komponenten komponentenImages: {}, // Store tiny pictures for komponenten
totalKomponentenPrice: 0, totalKomponentenPrice: 0,
totalSavings: 0 totalSavings: 0,
// Collapsible sections state
showQuestionForm: false,
showRatingForm: false,
showAvailabilityForm: false
}; };
} }
} }
@@ -459,6 +470,52 @@ class ProductDetailPage extends Component {
this.setState({ imageDialogOpen: false }); this.setState({ imageDialogOpen: false });
}; };
toggleQuestionForm = () => {
this.setState(prevState => ({
showQuestionForm: !prevState.showQuestionForm,
showRatingForm: false,
showAvailabilityForm: false
}), () => {
if (this.state.showQuestionForm) {
setTimeout(() => this.scrollToSection('question-form'), 100);
}
});
};
toggleRatingForm = () => {
this.setState(prevState => ({
showRatingForm: !prevState.showRatingForm,
showQuestionForm: false,
showAvailabilityForm: false
}), () => {
if (this.state.showRatingForm) {
setTimeout(() => this.scrollToSection('rating-form'), 100);
}
});
};
toggleAvailabilityForm = () => {
this.setState(prevState => ({
showAvailabilityForm: !prevState.showAvailabilityForm,
showQuestionForm: false,
showRatingForm: false
}), () => {
if (this.state.showAvailabilityForm) {
setTimeout(() => this.scrollToSection('availability-form'), 100);
}
});
};
scrollToSection = (sectionId) => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
};
render() { render() {
const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } = const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } =
this.state; this.state;
@@ -658,9 +715,10 @@ class ProductDetailPage extends Component {
</Box> </Box>
)} )}
{/* Attribute images and chips */} {/* Attribute images and chips with action buttons */}
{(attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert])) && ( {(attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert])) && (
<Stack direction="row" spacing={2} sx={{ flexWrap: "wrap", gap: 1, mb: 2 }}> <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2, gap: 2 }}>
<Stack direction="row" spacing={2} sx={{ flexWrap: "wrap", gap: 1, flex: 1 }}>
{attributes {attributes
.filter(attribute => attributeImages[attribute.kMerkmalWert]) .filter(attribute => attributeImages[attribute.kMerkmalWert])
.map((attribute) => { .map((attribute) => {
@@ -687,6 +745,61 @@ class ProductDetailPage extends Component {
/> />
))} ))}
</Stack> </Stack>
{/* Right-aligned action buttons */}
<Stack direction="column" spacing={1} sx={{ flexShrink: 0 }}>
<Button
variant="outlined"
size="small"
onClick={this.toggleQuestionForm}
sx={{
fontSize: "0.75rem",
px: 1.5,
py: 0.5,
minWidth: "auto",
whiteSpace: "nowrap"
}}
>
Frage zum Artikel
</Button>
<Button
variant="outlined"
size="small"
onClick={this.toggleRatingForm}
sx={{
fontSize: "0.75rem",
px: 1.5,
py: 0.5,
minWidth: "auto",
whiteSpace: "nowrap"
}}
>
Artikel Bewerten
</Button>
{(product.available !== 1 && product.availableSupplier !== 1) && (
<Button
variant="outlined"
size="small"
onClick={this.toggleAvailabilityForm}
sx={{
fontSize: "0.75rem",
px: 1.5,
py: 0.5,
minWidth: "auto",
whiteSpace: "nowrap",
borderColor: "warning.main",
color: "warning.main",
"&:hover": {
borderColor: "warning.dark",
backgroundColor: "warning.light"
}
}}
>
Verfügbarkeit anfragen
</Button>
)}
</Stack>
</Box>
)} )}
{/* Weight */} {/* Weight */}
@@ -888,6 +1001,39 @@ class ProductDetailPage extends Component {
</Box> </Box>
)} )}
{/* Article Question Form */}
<Collapse in={this.state.showQuestionForm}>
<div id="question-form">
<ArticleQuestionForm
productId={product.id}
productName={cleanProductName(product.name)}
socket={this.props.socket}
/>
</div>
</Collapse>
{/* Article Rating Form */}
<Collapse in={this.state.showRatingForm}>
<div id="rating-form">
<ArticleRatingForm
productId={product.id}
productName={cleanProductName(product.name)}
socket={this.props.socket}
/>
</div>
</Collapse>
{/* Article Availability Form - only show for out of stock items */}
{(product.available !== 1 && product.availableSupplier !== 1) && (
<Collapse in={this.state.showAvailabilityForm}>
<ArticleAvailabilityForm
productId={product.id}
productName={cleanProductName(product.name)}
socket={this.props.socket}
/>
</Collapse>
)}
{product.komponenten && product.komponenten.split(",").length > 0 && ( {product.komponenten && product.komponenten.split(",").length > 0 && (
<Box sx={{ mt: 4, p: 4, background: "#fff", borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}> <Box sx={{ mt: 4, p: 4, background: "#fff", borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h4" gutterBottom>{this.props.t ? this.props.t('product.consistsOf') : 'Bestehend aus:'}</Typography> <Typography variant="h4" gutterBottom>{this.props.t ? this.props.t('product.consistsOf') : 'Bestehend aus:'}</Typography>