diff --git a/src/components/ArticleAvailabilityForm.js b/src/components/ArticleAvailabilityForm.js
new file mode 100644
index 0000000..62596ec
--- /dev/null
+++ b/src/components/ArticleAvailabilityForm.js
@@ -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 (
+
+
+ Verfügbarkeit anfragen
+
+
+
+ Dieser Artikel ist derzeit nicht verfügbar. Gerne informieren wir Sie, sobald er wieder lieferbar ist.
+
+
+ {success && (
+
+ Vielen Dank für Ihre Anfrage! Wir werden Sie {notificationMethod === 'email' ? 'per E-Mail' : 'über Telegram'} informieren, sobald der Artikel wieder verfügbar ist.
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+ Wie möchten Sie benachrichtigt werden?
+
+
+ }
+ label="E-Mail"
+ />
+ }
+ label="Telegram Bot"
+ />
+
+
+
+ {notificationMethod === 'email' && (
+
+ )}
+
+ {notificationMethod === 'telegram' && (
+
+ )}
+
+
+
+
+
+
+ );
+ }
+}
+
+export default withI18n()(ArticleAvailabilityForm);
\ No newline at end of file
diff --git a/src/components/ArticleQuestionForm.js b/src/components/ArticleQuestionForm.js
new file mode 100644
index 0000000..dabd97a
--- /dev/null
+++ b/src/components/ArticleQuestionForm.js
@@ -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 (
+
+
+ Frage zum Artikel
+
+
+
+ Haben Sie eine Frage zu diesem Artikel? Wir helfen Ihnen gerne weiter.
+
+
+ {success && (
+
+ Vielen Dank für Ihre Frage! Wir werden uns schnellstmöglich bei Ihnen melden.
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default withI18n()(ArticleQuestionForm);
\ No newline at end of file
diff --git a/src/components/ArticleRatingForm.js b/src/components/ArticleRatingForm.js
new file mode 100644
index 0000000..b3bd91f
--- /dev/null
+++ b/src/components/ArticleRatingForm.js
@@ -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 (
+
+
+ Artikel Bewerten
+
+
+
+ Teilen Sie Ihre Erfahrungen mit diesem Artikel und helfen Sie anderen Kunden bei der Entscheidung.
+
+
+ {success && (
+
+ Vielen Dank für Ihre Bewertung! Sie wird nach Prüfung veröffentlicht.
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+
+
+ Bewertung *
+
+
+ }
+ />
+
+ {rating > 0 ? `${rating} von 5 Sternen` : 'Bitte bewerten'}
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default withI18n()(ArticleRatingForm);
\ No newline at end of file
diff --git a/src/components/PhotoUpload.js b/src/components/PhotoUpload.js
new file mode 100644
index 0000000..fc2e591
--- /dev/null
+++ b/src/components/PhotoUpload.js
@@ -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 (
+
+
+ {label || 'Fotos anhängen (optional)'}
+
+
+
+
+ }
+ onClick={() => this.fileInputRef.current?.click()}
+ disabled={disabled}
+ sx={{ mb: 2 }}
+ >
+ Fotos auswählen
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {previews.length > 0 && (
+
+ {previews.map((preview, index) => (
+
+
+
+ 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)'
+ }
+ }}
+ >
+
+
+
+ {preview.name}
+
+
+
+ ))}
+
+ )}
+
+ {files.length > 0 && (
+
+ {files.length} Datei(en) ausgewählt
+ {previews.length > 0 && previews.some(p => p.originalSize && p.compressedSize) && (
+
+ (komprimiert für Upload)
+
+ )}
+
+ )}
+
+ );
+ }
+}
+
+export default PhotoUpload;
\ No newline at end of file
diff --git a/src/components/ProductDetailPage.js b/src/components/ProductDetailPage.js
index b974394..1b7addd 100644
--- a/src/components/ProductDetailPage.js
+++ b/src/components/ProductDetailPage.js
@@ -1,10 +1,13 @@
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 parse from "html-react-parser";
import AddToCartButton from "./AddToCartButton.js";
import Images from "./Images.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
const cleanProductName = (name) => {
@@ -35,7 +38,11 @@ class ProductDetailPage extends Component {
komponentenData: {}, // Store individual komponent data with loading states
komponentenImages: {}, // Store tiny pictures for komponenten
totalKomponentenPrice: 0,
- totalSavings: 0
+ totalSavings: 0,
+ // Collapsible sections state
+ showQuestionForm: false,
+ showRatingForm: false,
+ showAvailabilityForm: false
};
} else {
this.state = {
@@ -51,7 +58,11 @@ class ProductDetailPage extends Component {
komponentenData: {}, // Store individual komponent data with loading states
komponentenImages: {}, // Store tiny pictures for komponenten
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 });
};
+ 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() {
const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } =
this.state;
@@ -658,35 +715,91 @@ class ProductDetailPage extends Component {
)}
- {/* Attribute images and chips */}
+ {/* Attribute images and chips with action buttons */}
{(attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert])) && (
-
- {attributes
- .filter(attribute => attributeImages[attribute.kMerkmalWert])
- .map((attribute) => {
- const key = attribute.kMerkmalWert;
- return (
-
-
-
- );
- })}
- {attributes
- .filter(attribute => !attributeImages[attribute.kMerkmalWert])
- .map((attribute) => (
-
- ))}
-
+
+
+ {attributes
+ .filter(attribute => attributeImages[attribute.kMerkmalWert])
+ .map((attribute) => {
+ const key = attribute.kMerkmalWert;
+ return (
+
+
+
+ );
+ })}
+ {attributes
+ .filter(attribute => !attributeImages[attribute.kMerkmalWert])
+ .map((attribute) => (
+
+ ))}
+
+
+ {/* Right-aligned action buttons */}
+
+
+
+ {(product.available !== 1 && product.availableSupplier !== 1) && (
+
+ )}
+
+
)}
{/* Weight */}
@@ -888,6 +1001,39 @@ class ProductDetailPage extends Component {
)}
+ {/* Article Question Form */}
+
+
+
+
+ {/* Article Rating Form */}
+
+
+
+
+ {/* Article Availability Form - only show for out of stock items */}
+ {(product.available !== 1 && product.availableSupplier !== 1) && (
+
+
+
+ )}
+
{product.komponenten && product.komponenten.split(",").length > 0 && (
{this.props.t ? this.props.t('product.consistsOf') : 'Bestehend aus:'}