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:
242
src/components/ArticleAvailabilityForm.js
Normal file
242
src/components/ArticleAvailabilityForm.js
Normal 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);
|
||||||
225
src/components/ArticleQuestionForm.js
Normal file
225
src/components/ArticleQuestionForm.js
Normal 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);
|
||||||
254
src/components/ArticleRatingForm.js
Normal file
254
src/components/ArticleRatingForm.js
Normal 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);
|
||||||
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;
|
||||||
@@ -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,35 +715,91 @@ 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 }}>
|
||||||
{attributes
|
<Stack direction="row" spacing={2} sx={{ flexWrap: "wrap", gap: 1, flex: 1 }}>
|
||||||
.filter(attribute => attributeImages[attribute.kMerkmalWert])
|
{attributes
|
||||||
.map((attribute) => {
|
.filter(attribute => attributeImages[attribute.kMerkmalWert])
|
||||||
const key = attribute.kMerkmalWert;
|
.map((attribute) => {
|
||||||
return (
|
const key = attribute.kMerkmalWert;
|
||||||
<Box key={key} sx={{ mb: 1,border: "1px solid #e0e0e0", borderRadius: 1 }}>
|
return (
|
||||||
<CardMedia
|
<Box key={key} sx={{ mb: 1,border: "1px solid #e0e0e0", borderRadius: 1 }}>
|
||||||
component="img"
|
<CardMedia
|
||||||
style={{ width: "72px", height: "98px" }}
|
component="img"
|
||||||
image={attributeImages[key]}
|
style={{ width: "72px", height: "98px" }}
|
||||||
alt={`Attribute ${key}`}
|
image={attributeImages[key]}
|
||||||
/>
|
alt={`Attribute ${key}`}
|
||||||
</Box>
|
/>
|
||||||
);
|
</Box>
|
||||||
})}
|
);
|
||||||
{attributes
|
})}
|
||||||
.filter(attribute => !attributeImages[attribute.kMerkmalWert])
|
{attributes
|
||||||
.map((attribute) => (
|
.filter(attribute => !attributeImages[attribute.kMerkmalWert])
|
||||||
<Chip
|
.map((attribute) => (
|
||||||
key={attribute.kMerkmalWert}
|
<Chip
|
||||||
label={attribute.cWert}
|
key={attribute.kMerkmalWert}
|
||||||
disabled
|
label={attribute.cWert}
|
||||||
sx={{ mb: 1 }}
|
disabled
|
||||||
/>
|
sx={{ mb: 1 }}
|
||||||
))}
|
/>
|
||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user