- Changed image file extensions from JPG to AVIF in data-fetching, product, category, and image components for improved performance and reduced file sizes. - Updated image blob creation to reflect the new AVIF format in various components, ensuring consistency in image handling throughout the application.
2013 lines
75 KiB
JavaScript
2013 lines
75 KiB
JavaScript
import React, { Component } from "react";
|
||
import { Box, Container, Typography, CardMedia, Stack, Chip, Button, Collapse, Popper, ClickAwayListener, MenuList, MenuItem, ListItemIcon, ListItemText, Snackbar, Alert } from "@mui/material";
|
||
import ShareIcon from "@mui/icons-material/Share";
|
||
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
|
||
import FacebookIcon from "@mui/icons-material/Facebook";
|
||
import TelegramIcon from "@mui/icons-material/Telegram";
|
||
import EmailIcon from "@mui/icons-material/Email";
|
||
import LinkIcon from "@mui/icons-material/Link";
|
||
import CodeIcon from "@mui/icons-material/Code";
|
||
import { Link } from "react-router-dom";
|
||
import parse from "html-react-parser";
|
||
import sanitizeHtml from "sanitize-html";
|
||
import AddToCartButton from "./AddToCartButton.js";
|
||
import ProductImage from "./ProductImage.js";
|
||
import Product from "./Product.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) => {
|
||
if (!name) return "";
|
||
// Remove patterns like " (1)", " (3)", " (10)" at the end of the string
|
||
return name.replace(/\s*\(\d+\)\s*$/, "").trim();
|
||
};
|
||
|
||
// Product detail page with image loading
|
||
class ProductDetailPage extends Component {
|
||
constructor(props) {
|
||
super(props);
|
||
|
||
// First try to find cached data by seoName (complete data)
|
||
let cachedData = null;
|
||
let partialProduct = null;
|
||
let isUpgrading = false;
|
||
|
||
// Get current language for cache key
|
||
const currentLanguage = this.props.i18n?.language || 'de';
|
||
const cacheKey = `product_${this.props.seoName}_${currentLanguage}`;
|
||
|
||
if (window.productDetailCache && window.productDetailCache[cacheKey]) {
|
||
cachedData = window.productDetailCache[cacheKey];
|
||
} else if (window.productDetailCache) {
|
||
// If not found by seoName, search for partial data by checking all cached products
|
||
// Look for a product where the seoName matches this.props.seoName
|
||
for (const key in window.productDetailCache) {
|
||
const cached = window.productDetailCache[key];
|
||
if (cached && cached.product && cached.product.seoName === this.props.seoName) {
|
||
partialProduct = cached.product;
|
||
isUpgrading = true;
|
||
break;
|
||
}
|
||
// Also check if cached is a product object directly (from category cache)
|
||
if (cached && typeof cached === 'object' && cached.seoName === this.props.seoName) {
|
||
partialProduct = cached;
|
||
isUpgrading = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (cachedData) {
|
||
// Complete cached data found
|
||
|
||
// Clean up prerender fallback since we have cached data
|
||
if (typeof window !== "undefined" && window.__PRERENDER_FALLBACK__) {
|
||
delete window.__PRERENDER_FALLBACK__;
|
||
console.log("ProductDetailPage: Cleaned up prerender fallback using cached product data");
|
||
}
|
||
|
||
// Initialize komponenten from cached product data
|
||
const komponenten = [];
|
||
if(cachedData.product.komponenten) {
|
||
for(const komponent of cachedData.product.komponenten.split(",")) {
|
||
// Handle both "x" and "×" as separators
|
||
const [id, count] = komponent.split(/[x×]/);
|
||
komponenten.push({id: id.trim(), count: count.trim()});
|
||
}
|
||
}
|
||
|
||
this.state = {
|
||
product: cachedData.product,
|
||
loading: false,
|
||
upgrading: false,
|
||
error: null,
|
||
attributeImages: {},
|
||
attributes: cachedData.attributes || [],
|
||
isSteckling: false,
|
||
imageDialogOpen: false,
|
||
komponenten: komponenten,
|
||
komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded
|
||
komponentenData: {}, // Store individual komponent data with loading states
|
||
komponentenImages: {}, // Store tiny pictures for komponenten
|
||
totalKomponentenPrice: 0,
|
||
totalSavings: 0,
|
||
// Embedded products from <product> tags in description
|
||
embeddedProducts: {},
|
||
embeddedProductImages: {},
|
||
// Collapsible sections state
|
||
showQuestionForm: false,
|
||
showRatingForm: false,
|
||
showAvailabilityForm: false,
|
||
// Share popper state
|
||
shareAnchorEl: null,
|
||
sharePopperOpen: false,
|
||
// Snackbar state
|
||
snackbarOpen: false,
|
||
snackbarMessage: "",
|
||
snackbarSeverity: "success",
|
||
// Similar products
|
||
similarProducts: cachedData.similarProducts || []
|
||
};
|
||
} else if (partialProduct && isUpgrading) {
|
||
// Partial product data found - enter upgrading state
|
||
console.log("ProductDetailPage: Found partial product data, entering upgrading state");
|
||
|
||
// Clean up prerender fallback since we have some data
|
||
if (typeof window !== "undefined" && window.__PRERENDER_FALLBACK__) {
|
||
delete window.__PRERENDER_FALLBACK__;
|
||
console.log("ProductDetailPage: Cleaned up prerender fallback using partial product data");
|
||
}
|
||
|
||
// Initialize komponenten from partial product data if available
|
||
const komponenten = [];
|
||
if(partialProduct.komponenten) {
|
||
for(const komponent of partialProduct.komponenten.split(",")) {
|
||
// Handle both "x" and "×" as separators
|
||
const [id, count] = komponent.split(/[x×]/);
|
||
komponenten.push({id: id.trim(), count: count.trim()});
|
||
}
|
||
}
|
||
|
||
this.state = {
|
||
product: partialProduct,
|
||
loading: false,
|
||
upgrading: true, // This indicates we have partial data and are loading complete data
|
||
error: null,
|
||
attributeImages: {},
|
||
attributes: [], // Will be loaded when upgrading
|
||
isSteckling: false,
|
||
imageDialogOpen: false,
|
||
komponenten: komponenten,
|
||
komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded
|
||
komponentenData: {}, // Store individual komponent data with loading states
|
||
komponentenImages: {}, // Store tiny pictures for komponenten
|
||
totalKomponentenPrice: 0,
|
||
totalSavings: 0,
|
||
// Embedded products from <product> tags in description
|
||
embeddedProducts: {},
|
||
embeddedProductImages: {},
|
||
// Collapsible sections state
|
||
showQuestionForm: false,
|
||
showRatingForm: false,
|
||
showAvailabilityForm: false,
|
||
// Share popper state
|
||
shareAnchorEl: null,
|
||
sharePopperOpen: false,
|
||
// Snackbar state
|
||
snackbarOpen: false,
|
||
snackbarMessage: "",
|
||
snackbarSeverity: "success",
|
||
// Similar products
|
||
similarProducts: []
|
||
};
|
||
} else {
|
||
// No cached data found - full loading state
|
||
this.state = {
|
||
product: null,
|
||
loading: true,
|
||
upgrading: false,
|
||
error: null,
|
||
attributeImages: {},
|
||
attributes: [],
|
||
isSteckling: false,
|
||
imageDialogOpen: false,
|
||
komponenten: [],
|
||
komponentenLoaded: false,
|
||
komponentenData: {}, // Store individual komponent data with loading states
|
||
komponentenImages: {}, // Store tiny pictures for komponenten
|
||
totalKomponentenPrice: 0,
|
||
totalSavings: 0,
|
||
// Embedded products from <product> tags in description
|
||
embeddedProducts: {},
|
||
embeddedProductImages: {},
|
||
// Collapsible sections state
|
||
showQuestionForm: false,
|
||
showRatingForm: false,
|
||
showAvailabilityForm: false,
|
||
// Share popper state
|
||
shareAnchorEl: null,
|
||
sharePopperOpen: false,
|
||
// Snackbar state
|
||
snackbarOpen: false,
|
||
snackbarMessage: "",
|
||
snackbarSeverity: "success",
|
||
// Similar products
|
||
similarProducts: []
|
||
};
|
||
}
|
||
}
|
||
|
||
componentDidMount() {
|
||
// Update context with cached product if available
|
||
if (this.state.product && this.props.setCurrentProduct) {
|
||
console.log('ProductDetailPage: Setting product context from cache', this.state.product.name);
|
||
this.props.setCurrentProduct({
|
||
name: this.state.product.name,
|
||
categoryId: this.state.product.kategorien ? this.state.product.kategorien.split(',')[0] : undefined
|
||
});
|
||
} else if (this.state.product) {
|
||
console.warn('ProductDetailPage: setCurrentProduct prop is missing despite having product');
|
||
}
|
||
|
||
// Load product data if we have no product or if we're in upgrading state
|
||
if (!this.state.product || this.state.upgrading) {
|
||
this.loadProductData();
|
||
} else {
|
||
// Product is cached, but we still need to load attribute images and komponenten
|
||
if (this.state.attributes && this.state.attributes.length > 0) {
|
||
this.loadAttributeImages(this.state.attributes);
|
||
}
|
||
|
||
if (this.state.komponenten.length > 0 && !this.state.komponentenLoaded) {
|
||
for(const komponent of this.state.komponenten) {
|
||
this.loadKomponent(komponent.id, komponent.count);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
componentDidUpdate(prevProps) {
|
||
// Check for seoName changes
|
||
if (prevProps.seoName !== this.props.seoName) {
|
||
// Clear context when navigating to new product
|
||
if (this.props.setCurrentProduct) {
|
||
this.props.setCurrentProduct(null);
|
||
}
|
||
|
||
this.setState(
|
||
{ product: null, loading: true, upgrading: false, error: null, imageDialogOpen: false, similarProducts: [] },
|
||
this.loadProductData
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Check for language changes
|
||
const prevLanguage = prevProps.languageContext?.currentLanguage || prevProps.i18n?.language || 'de';
|
||
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||
|
||
if (prevLanguage !== currentLanguage) {
|
||
console.log('Language changed from', prevLanguage, 'to', currentLanguage, '- reloading product data');
|
||
|
||
// Clear caches globally to force fresh socket calls with new language
|
||
if (typeof window !== 'undefined') {
|
||
window.productCache = {};
|
||
window.productDetailCache = {};
|
||
}
|
||
|
||
// Reset component state and reload data
|
||
this.setState(
|
||
{
|
||
loading: false,
|
||
upgrading: false,
|
||
error: null,
|
||
imageDialogOpen: false,
|
||
komponentenData: {},
|
||
komponentenLoaded: false,
|
||
totalKomponentenPrice: 0,
|
||
totalSavings: 0,
|
||
similarProducts: []
|
||
},
|
||
this.loadProductData
|
||
);
|
||
}
|
||
}
|
||
|
||
loadKomponentImage = (komponentId, pictureList) => {
|
||
// Initialize cache if it doesn't exist
|
||
if (!window.smallPicCache) {
|
||
window.smallPicCache = {};
|
||
}
|
||
|
||
// Skip if no pictureList
|
||
if (!pictureList || pictureList.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// Get the first image ID from pictureList
|
||
const bildId = pictureList.split(',')[0];
|
||
|
||
// Check if already cached
|
||
if (window.smallPicCache[bildId]) {
|
||
this.setState(prevState => ({
|
||
komponentenImages: {
|
||
...prevState.komponentenImages,
|
||
[komponentId]: window.smallPicCache[bildId]
|
||
}
|
||
}));
|
||
return;
|
||
}
|
||
|
||
// Fetch image from server
|
||
console.log('loadKomponentImage', bildId);
|
||
window.socketManager.emit('getPic', { bildId, size: 'small' }, (res) => {
|
||
if (res.success) {
|
||
// Cache the image
|
||
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
|
||
|
||
// Update state
|
||
this.setState(prevState => ({
|
||
komponentenImages: {
|
||
...prevState.komponentenImages,
|
||
[komponentId]: window.smallPicCache[bildId]
|
||
}
|
||
}));
|
||
} else {
|
||
console.log('Error loading komponent image:', res);
|
||
}
|
||
});
|
||
}
|
||
|
||
loadKomponent = (id, count) => {
|
||
// Initialize cache if it doesn't exist
|
||
if (!window.productDetailCache) {
|
||
window.productDetailCache = {};
|
||
}
|
||
|
||
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||
const cacheKey = `product_${id}_${currentLanguage}`;
|
||
|
||
// Check if this komponent is already cached
|
||
if (window.productDetailCache[cacheKey]) {
|
||
const cachedProduct = window.productDetailCache[cacheKey];
|
||
|
||
// Load komponent image if available
|
||
if (cachedProduct.pictureList) {
|
||
this.loadKomponentImage(id, cachedProduct.pictureList);
|
||
}
|
||
|
||
// Update state with cached data
|
||
this.setState(prevState => {
|
||
const newKomponentenData = {
|
||
...prevState.komponentenData,
|
||
[id]: {
|
||
...cachedProduct,
|
||
count: parseInt(count),
|
||
loaded: true
|
||
}
|
||
};
|
||
|
||
// Check if all remaining komponenten are loaded
|
||
const allLoaded = prevState.komponenten.every(k =>
|
||
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||
);
|
||
|
||
// Calculate totals if all loaded
|
||
let totalKomponentenPrice = 0;
|
||
let totalSavings = 0;
|
||
|
||
if (allLoaded) {
|
||
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
|
||
const komponentData = newKomponentenData[k.id];
|
||
if (komponentData && komponentData.loaded) {
|
||
return sum + (komponentData.price * parseInt(k.count));
|
||
}
|
||
return sum;
|
||
}, 0);
|
||
|
||
// Calculate savings (difference between buying individually vs as set)
|
||
const setPrice = prevState.product ? prevState.product.price : 0;
|
||
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||
}
|
||
|
||
console.log("Cached komponent loaded:", id, "data:", newKomponentenData[id]);
|
||
console.log("All loaded (cached):", allLoaded);
|
||
|
||
return {
|
||
komponentenData: newKomponentenData,
|
||
komponentenLoaded: allLoaded,
|
||
totalKomponentenPrice,
|
||
totalSavings
|
||
};
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
// Mark this komponent as loading
|
||
this.setState(prevState => ({
|
||
komponentenData: {
|
||
...prevState.komponentenData,
|
||
[id]: {
|
||
...prevState.komponentenData[id],
|
||
loading: true,
|
||
loaded: false,
|
||
count: parseInt(count)
|
||
}
|
||
}
|
||
}));
|
||
|
||
console.log('loadKomponent', id, count);
|
||
|
||
window.socketManager.emit(
|
||
"getProductView",
|
||
{ articleId: id, language: currentLanguage, requestTranslation: currentLanguage === "de" ? false : true},
|
||
(res) => {
|
||
if (res.success) {
|
||
// Use translated product if available, otherwise use original product
|
||
const productData = res.translatedProduct || res.product;
|
||
|
||
// Cache the successful response
|
||
window.productDetailCache[cacheKey] = productData;
|
||
|
||
// Load komponent image if available
|
||
if (productData.pictureList) {
|
||
this.loadKomponentImage(id, productData.pictureList);
|
||
}
|
||
|
||
// Update state with loaded data
|
||
this.setState(prevState => {
|
||
const newKomponentenData = {
|
||
...prevState.komponentenData,
|
||
[id]: {
|
||
...productData,
|
||
count: parseInt(count),
|
||
loading: false,
|
||
loaded: true
|
||
}
|
||
};
|
||
|
||
// Check if all remaining komponenten are loaded
|
||
const allLoaded = prevState.komponenten.every(k =>
|
||
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||
);
|
||
|
||
// Calculate totals if all loaded
|
||
let totalKomponentenPrice = 0;
|
||
let totalSavings = 0;
|
||
|
||
if (allLoaded) {
|
||
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
|
||
const komponentData = newKomponentenData[k.id];
|
||
if (komponentData && komponentData.loaded) {
|
||
return sum + (komponentData.price * parseInt(k.count));
|
||
}
|
||
return sum;
|
||
}, 0);
|
||
|
||
// Calculate savings (difference between buying individually vs as set)
|
||
const setPrice = prevState.product ? prevState.product.price : 0;
|
||
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||
}
|
||
|
||
console.log("Updated komponentenData for", id, ":", newKomponentenData[id]);
|
||
console.log("All loaded:", allLoaded);
|
||
|
||
return {
|
||
komponentenData: newKomponentenData,
|
||
komponentenLoaded: allLoaded,
|
||
totalKomponentenPrice,
|
||
totalSavings
|
||
};
|
||
});
|
||
|
||
console.log("getProductView (komponent)", res);
|
||
} else {
|
||
console.error("Error loading komponent:", res.error || "Unknown error", res);
|
||
|
||
// Remove failed komponent from the list and check if all remaining are loaded
|
||
this.setState(prevState => {
|
||
const newKomponenten = prevState.komponenten.filter(k => k.id !== id);
|
||
const newKomponentenData = { ...prevState.komponentenData };
|
||
|
||
// Remove failed komponent from data
|
||
delete newKomponentenData[id];
|
||
|
||
// Check if all remaining komponenten are loaded
|
||
const allLoaded = newKomponenten.length === 0 || newKomponenten.every(k =>
|
||
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||
);
|
||
|
||
// Calculate totals if all loaded
|
||
let totalKomponentenPrice = 0;
|
||
let totalSavings = 0;
|
||
|
||
if (allLoaded && newKomponenten.length > 0) {
|
||
totalKomponentenPrice = newKomponenten.reduce((sum, k) => {
|
||
const komponentData = newKomponentenData[k.id];
|
||
if (komponentData && komponentData.loaded) {
|
||
return sum + (komponentData.price * parseInt(k.count));
|
||
}
|
||
return sum;
|
||
}, 0);
|
||
|
||
// Calculate savings (difference between buying individually vs as set)
|
||
const setPrice = this.state.product ? this.state.product.price : 0;
|
||
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||
}
|
||
|
||
console.log("Removed failed komponent:", id, "remaining:", newKomponenten.length);
|
||
console.log("All loaded after removal:", allLoaded);
|
||
|
||
return {
|
||
komponenten: newKomponenten,
|
||
komponentenData: newKomponentenData,
|
||
komponentenLoaded: allLoaded,
|
||
totalKomponentenPrice,
|
||
totalSavings
|
||
};
|
||
});
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
loadAttributeImages = (attributes) => {
|
||
// Initialize window-level attribute image cache if it doesn't exist
|
||
if (!window.attributeImageCache) {
|
||
window.attributeImageCache = {};
|
||
}
|
||
|
||
if (attributes && attributes.length > 0) {
|
||
const attributeImages = {};
|
||
|
||
for (const attribute of attributes) {
|
||
const cacheKey = attribute.kMerkmalWert;
|
||
|
||
if (attribute.cName == "Anzahl")
|
||
this.setState({ isSteckling: true });
|
||
|
||
// Check if we have a cached result (either URL or negative result)
|
||
if (window.attributeImageCache[cacheKey]) {
|
||
const cached = window.attributeImageCache[cacheKey];
|
||
if (cached.url) {
|
||
// Use cached URL
|
||
attributeImages[cacheKey] = cached.url;
|
||
}
|
||
} else {
|
||
// Not in cache, fetch from server
|
||
console.log('getAttributePicture', cacheKey);
|
||
window.socketManager.emit(
|
||
"getAttributePicture",
|
||
{ id: cacheKey },
|
||
(res) => {
|
||
console.log("getAttributePicture", res);
|
||
if (res.success && !res.noPicture) {
|
||
const blob = new Blob([res.imageBuffer], {
|
||
type: "image/avif",
|
||
});
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
// Cache the successful URL
|
||
window.attributeImageCache[cacheKey] = {
|
||
url: url,
|
||
timestamp: Date.now(),
|
||
};
|
||
|
||
// Update state and force re-render
|
||
this.setState(prevState => ({
|
||
attributeImages: {
|
||
...prevState.attributeImages,
|
||
[cacheKey]: url
|
||
}
|
||
}));
|
||
} else {
|
||
// Cache negative result to avoid future requests
|
||
// This handles both failure cases and success with noPicture: true
|
||
window.attributeImageCache[cacheKey] = {
|
||
noImage: true,
|
||
timestamp: Date.now(),
|
||
};
|
||
}
|
||
}
|
||
);
|
||
}
|
||
}
|
||
|
||
// Set initial state with cached images
|
||
if (Object.keys(attributeImages).length > 0) {
|
||
this.setState({ attributeImages });
|
||
}
|
||
}
|
||
}
|
||
|
||
loadProductData = () => {
|
||
console.log('loadProductData', this.props.seoName);
|
||
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||
console.log('debuglanguage', currentLanguage);
|
||
|
||
const cacheKey = `product_${this.props.seoName}_${currentLanguage}`;
|
||
|
||
window.socketManager.emit(
|
||
"getProductView",
|
||
{ seoName: this.props.seoName, language: currentLanguage, requestTranslation: currentLanguage === "de" ? false : true},
|
||
(res) => {
|
||
if (res.success) {
|
||
// Use translated product if available, otherwise use original product
|
||
const productData = res.translatedProduct || res.product;
|
||
productData.seoName = this.props.seoName;
|
||
|
||
// Initialize cache if it doesn't exist
|
||
if (!window.productDetailCache) {
|
||
window.productDetailCache = {};
|
||
}
|
||
|
||
// Cache the complete response data (product + attributes) - cache the response with translated product
|
||
const cacheData = { ...res, product: productData };
|
||
window.productDetailCache[cacheKey] = cacheData;
|
||
|
||
// Clean up prerender fallback since we now have real data
|
||
if (typeof window !== "undefined" && window.__PRERENDER_FALLBACK__) {
|
||
delete window.__PRERENDER_FALLBACK__;
|
||
console.log("ProductDetailPage: Cleaned up prerender fallback after loading product data");
|
||
}
|
||
|
||
const komponenten = [];
|
||
if(productData.komponenten) {
|
||
for(const komponent of productData.komponenten.split(",")) {
|
||
// Handle both "x" and "×" as separators
|
||
const [id, count] = komponent.split(/[x×]/);
|
||
komponenten.push({id: id.trim(), count: count.trim()});
|
||
}
|
||
}
|
||
this.setState({
|
||
product: productData,
|
||
loading: false,
|
||
upgrading: false, // Clear upgrading state since we now have complete data
|
||
error: null,
|
||
imageDialogOpen: false,
|
||
attributes: res.attributes,
|
||
komponenten: komponenten,
|
||
komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded
|
||
similarProducts: res.similarProducts || []
|
||
}, () => {
|
||
// Update context
|
||
if (this.props.setCurrentProduct) {
|
||
console.log('ProductDetailPage: Setting product context from fetch', productData.name);
|
||
this.props.setCurrentProduct({
|
||
name: productData.name,
|
||
categoryId: productData.kategorien ? productData.kategorien.split(',')[0] : undefined
|
||
});
|
||
} else {
|
||
console.warn('ProductDetailPage: setCurrentProduct prop is missing after fetch');
|
||
}
|
||
|
||
if(komponenten.length > 0) {
|
||
for(const komponent of komponenten) {
|
||
this.loadKomponent(komponent.id, komponent.count);
|
||
}
|
||
}
|
||
});
|
||
console.log("getProductView", res);
|
||
|
||
// Load attribute images
|
||
this.loadAttributeImages(res.attributes);
|
||
} else {
|
||
console.error(
|
||
"Error loading product:",
|
||
res.error || "Unknown error",
|
||
res
|
||
);
|
||
this.setState({
|
||
product: null,
|
||
loading: false,
|
||
error: "Error loading product",
|
||
imageDialogOpen: false,
|
||
});
|
||
}
|
||
}
|
||
);
|
||
};
|
||
|
||
handleOpenDialog = () => {
|
||
this.setState({ imageDialogOpen: true });
|
||
};
|
||
|
||
handleCloseDialog = () => {
|
||
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'
|
||
});
|
||
}
|
||
};
|
||
|
||
// Share functionality
|
||
handleShareClick = (event) => {
|
||
this.setState({
|
||
shareAnchorEl: event.currentTarget,
|
||
sharePopperOpen: true
|
||
});
|
||
};
|
||
|
||
handleShareClose = () => {
|
||
this.setState({
|
||
shareAnchorEl: null,
|
||
sharePopperOpen: false
|
||
});
|
||
};
|
||
|
||
showSnackbar = (message, severity = "success") => {
|
||
this.setState({
|
||
snackbarOpen: true,
|
||
snackbarMessage: message,
|
||
snackbarSeverity: severity
|
||
});
|
||
};
|
||
|
||
handleSnackbarClose = (event, reason) => {
|
||
if (reason === 'clickaway') {
|
||
return;
|
||
}
|
||
this.setState({ snackbarOpen: false });
|
||
};
|
||
|
||
getProductUrl = () => {
|
||
return `${window.location.origin}/Artikel/${this.props.seoName}`;
|
||
};
|
||
|
||
handleEmbedShare = () => {
|
||
const embedCode = `<iframe src="${this.getProductUrl()}" width="100%" height="600" frameborder="0"></iframe>`;
|
||
navigator.clipboard.writeText(embedCode).then(() => {
|
||
this.showSnackbar("Einbettungscode wurde in die Zwischenablage kopiert!");
|
||
}).catch(() => {
|
||
// Fallback for older browsers
|
||
try {
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = embedCode;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textArea);
|
||
this.showSnackbar("Einbettungscode wurde in die Zwischenablage kopiert!");
|
||
} catch {
|
||
this.showSnackbar("Fehler beim Kopieren des Einbettungscodes", "error");
|
||
}
|
||
});
|
||
this.handleShareClose();
|
||
};
|
||
|
||
handleWhatsAppShare = () => {
|
||
const url = this.getProductUrl();
|
||
const text = `Schau dir dieses Produkt an: ${cleanProductName(this.state.product.name)}`;
|
||
const whatsappUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`;
|
||
window.open(whatsappUrl, '_blank');
|
||
this.handleShareClose();
|
||
};
|
||
|
||
handleFacebookShare = () => {
|
||
const url = this.getProductUrl();
|
||
const facebookUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`;
|
||
window.open(facebookUrl, '_blank');
|
||
this.handleShareClose();
|
||
};
|
||
|
||
handleTelegramShare = () => {
|
||
const url = this.getProductUrl();
|
||
const text = `Schau dir dieses Produkt an: ${cleanProductName(this.state.product.name)}`;
|
||
const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`;
|
||
window.open(telegramUrl, '_blank');
|
||
this.handleShareClose();
|
||
};
|
||
|
||
handleEmailShare = () => {
|
||
const url = this.getProductUrl();
|
||
const subject = `Produktempfehlung: ${cleanProductName(this.state.product.name)}`;
|
||
const body = `Hallo,\n\nich möchte dir dieses Produkt empfehlen:\n\n${cleanProductName(this.state.product.name)}\n${url}\n\nViele Grüße`;
|
||
const emailUrl = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||
window.location.href = emailUrl;
|
||
this.handleShareClose();
|
||
};
|
||
|
||
handleLinkCopy = () => {
|
||
const url = this.getProductUrl();
|
||
navigator.clipboard.writeText(url).then(() => {
|
||
this.showSnackbar("Link wurde in die Zwischenablage kopiert!");
|
||
}).catch(() => {
|
||
// Fallback for older browsers
|
||
try {
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = url;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textArea);
|
||
this.showSnackbar("Link wurde in die Zwischenablage kopiert!");
|
||
} catch {
|
||
this.showSnackbar("Fehler beim Kopieren des Links", "error");
|
||
}
|
||
});
|
||
this.handleShareClose();
|
||
};
|
||
|
||
// Render embedded product from <product articlenr="..."> tag in description
|
||
renderEmbeddedProduct = (articleNr) => {
|
||
console.log('renderEmbeddedProduct called with articleNr:', articleNr);
|
||
|
||
// Check if we already have this product data in state
|
||
const embeddedProducts = this.state.embeddedProducts || {};
|
||
const productData = embeddedProducts[articleNr];
|
||
|
||
console.log('Embedded product data:', productData);
|
||
|
||
// If there was an error loading, show error message (don't retry infinitely)
|
||
if (productData && productData.error) {
|
||
return (
|
||
<Box
|
||
key={`embedded-${articleNr}`}
|
||
sx={{
|
||
my: 2,
|
||
p: 2,
|
||
background: "#fff3f3",
|
||
borderRadius: 2,
|
||
border: "1px solid #ffcdd2"
|
||
}}
|
||
>
|
||
<Typography variant="body2" color="error">
|
||
Produkt nicht gefunden (Artikelnr: {articleNr})
|
||
</Typography>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
if (!productData || !productData.loaded) {
|
||
// If not loaded yet and not currently loading, fetch it
|
||
if (!productData || (!productData.loading && !productData.error)) {
|
||
console.log('Starting to load embedded product:', articleNr);
|
||
this.loadEmbeddedProduct(articleNr);
|
||
}
|
||
|
||
// Return loading state
|
||
return (
|
||
<Box
|
||
key={`embedded-${articleNr}`}
|
||
sx={{
|
||
my: 2,
|
||
p: 2,
|
||
background: "#f9f9f9",
|
||
borderRadius: 2,
|
||
border: "1px solid #e0e0e0"
|
||
}}
|
||
>
|
||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||
<Box sx={{ width: 60, height: 60, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||
{/* Empty placeholder for image */}
|
||
</Box>
|
||
<Box>
|
||
<Typography variant="body1">
|
||
{this.props.t('product.loadingProduct')}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{this.props.t('product.articleNumber')}: {articleNr}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
// Product data is loaded, render it
|
||
const embeddedImages = this.state.embeddedProductImages || {};
|
||
const productImage = embeddedImages[articleNr];
|
||
|
||
return (
|
||
<Box
|
||
key={`embedded-${articleNr}`}
|
||
component={Link}
|
||
to={`/Artikel/${productData.seoName}`}
|
||
sx={{
|
||
display: "block",
|
||
my: 2,
|
||
p: 2,
|
||
background: "#f9f9f9",
|
||
borderRadius: 2,
|
||
border: "1px solid #e0e0e0",
|
||
textDecoration: "none",
|
||
color: "inherit",
|
||
transition: "all 0.2s",
|
||
"&:hover": {
|
||
backgroundColor: "#f0f0f0",
|
||
boxShadow: "0 2px 8px rgba(0,0,0,0.1)"
|
||
}
|
||
}}
|
||
>
|
||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||
<Box sx={{ width: 60, height: 60, flexShrink: 0 }}>
|
||
{productImage ? (
|
||
<CardMedia
|
||
component="img"
|
||
height="60"
|
||
image={productImage}
|
||
alt={productData.name}
|
||
sx={{
|
||
objectFit: "contain",
|
||
borderRadius: 1,
|
||
border: "1px solid #e0e0e0"
|
||
}}
|
||
/>
|
||
) : (
|
||
<CardMedia
|
||
component="img"
|
||
height="60"
|
||
image="/assets/images/nopicture.jpg"
|
||
alt={productData.name}
|
||
sx={{
|
||
objectFit: "contain",
|
||
borderRadius: 1,
|
||
border: "1px solid #e0e0e0"
|
||
}}
|
||
/>
|
||
)}
|
||
</Box>
|
||
<Box sx={{ flex: 1 }}>
|
||
<Typography variant="body1" sx={{ fontWeight: 500, mb: 0.5 }}>
|
||
{cleanProductName(productData.name)}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{this.props.t('product.articleNumber')}: {productData.articleNumber}
|
||
</Typography>
|
||
</Box>
|
||
<Box sx={{ textAlign: "right" }}>
|
||
<Typography variant="h6" sx={{ fontWeight: 600, color: "primary.main" }}>
|
||
{new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(productData.price)}
|
||
</Typography>
|
||
<Typography variant="caption" color="text.secondary">
|
||
inkl. MwSt.
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
// Load embedded product data by article number
|
||
loadEmbeddedProduct = (articleNr) => {
|
||
console.log('loadEmbeddedProduct', articleNr);
|
||
|
||
// Mark as loading
|
||
this.setState(prevState => ({
|
||
embeddedProducts: {
|
||
...prevState.embeddedProducts,
|
||
[articleNr]: { loading: true, loaded: false }
|
||
}
|
||
}));
|
||
|
||
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||
|
||
// Fetch product data from API using getProductView (same as komponenten)
|
||
window.socketManager.emit('getProductView', {
|
||
articleNr: articleNr,
|
||
language: currentLanguage,
|
||
requestTranslation: currentLanguage === 'de' ? false : true
|
||
}, (response) => {
|
||
console.log('loadEmbeddedProduct response:', articleNr, response);
|
||
|
||
if (response.success && response.product) {
|
||
// Use translated product if available, otherwise use original product
|
||
const product = response.translatedProduct || response.product;
|
||
|
||
console.log('Successfully loaded embedded product:', articleNr, product.name);
|
||
|
||
// Update state with loaded product data
|
||
this.setState(prevState => ({
|
||
embeddedProducts: {
|
||
...prevState.embeddedProducts,
|
||
[articleNr]: {
|
||
...product,
|
||
loading: false,
|
||
loaded: true
|
||
}
|
||
}
|
||
}));
|
||
|
||
// Load product image if available
|
||
if (product.pictureList && product.pictureList.length > 0) {
|
||
const bildId = product.pictureList.split(',')[0];
|
||
this.loadEmbeddedProductImage(articleNr, bildId);
|
||
}
|
||
} else {
|
||
console.warn(`Failed to load embedded product ${articleNr}:`, response);
|
||
// Mark as failed to load
|
||
this.setState(prevState => ({
|
||
embeddedProducts: {
|
||
...prevState.embeddedProducts,
|
||
[articleNr]: {
|
||
loading: false,
|
||
loaded: false,
|
||
error: true,
|
||
errorMessage: response.error || 'Unknown error'
|
||
}
|
||
}
|
||
}));
|
||
}
|
||
});
|
||
};
|
||
|
||
// Load embedded product image
|
||
loadEmbeddedProductImage = (articleNr, bildId) => {
|
||
console.log('loadEmbeddedProductImage', articleNr, bildId);
|
||
|
||
window.socketManager.emit('getPic', { bildId, size: 'small' }, (res) => {
|
||
console.log('loadEmbeddedProductImage response:', articleNr, res.success);
|
||
|
||
if (res.success) {
|
||
const imageUrl = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
|
||
this.setState(prevState => {
|
||
console.log('Setting embedded product image for', articleNr);
|
||
return {
|
||
embeddedProductImages: {
|
||
...prevState.embeddedProductImages,
|
||
[articleNr]: imageUrl
|
||
}
|
||
};
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
componentWillUnmount() {
|
||
if (this.props.setCurrentProduct) {
|
||
this.props.setCurrentProduct(null);
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const { product, loading, upgrading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings, shareAnchorEl, sharePopperOpen, snackbarOpen, snackbarMessage, snackbarSeverity } =
|
||
this.state;
|
||
|
||
// Debug alerts removed
|
||
|
||
|
||
|
||
if (loading && !upgrading) {
|
||
// Only show full loading screen when we have no product data at all
|
||
// Check if prerender fallback is available
|
||
if (typeof window !== "undefined" && window.__PRERENDER_FALLBACK__) {
|
||
return (
|
||
<div
|
||
dangerouslySetInnerHTML={{
|
||
__html: window.__PRERENDER_FALLBACK__.content,
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// Fallback to blank page if no prerender content
|
||
return <div style={{ minHeight: "60vh" }} />;
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<Box sx={{ p: 4, textAlign: "center" }}>
|
||
<Typography variant="h2" component="h2" gutterBottom color="error">
|
||
Fehler
|
||
</Typography>
|
||
<Typography>{error}</Typography>
|
||
<Link to="/" style={{ textDecoration: "none" }}>
|
||
<Typography color="primary" sx={{ mt: 2 }}>
|
||
{this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'}
|
||
</Typography>
|
||
</Link>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
if (!product) {
|
||
return (
|
||
<Box sx={{ p: 4, textAlign: "center" }}>
|
||
<Typography variant="h2" component="h2" gutterBottom>
|
||
Produkt nicht gefunden
|
||
</Typography>
|
||
<Typography>
|
||
Das gesuchte Produkt existiert nicht oder wurde entfernt.
|
||
</Typography>
|
||
<Link to="/" style={{ textDecoration: "none" }}>
|
||
<Typography color="primary" sx={{ mt: 2 }}>
|
||
{this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'}
|
||
</Typography>
|
||
</Link>
|
||
</Box>
|
||
);
|
||
}
|
||
// Format price with tax
|
||
const priceWithTax = new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(product.price);
|
||
|
||
let originalPriceWithTax = null;
|
||
if (product.rebate && product.rebate > 0) {
|
||
const rebatePct = product.rebate / 100;
|
||
const originalPrice = Math.round((product.price / (1 - rebatePct)) * 10) / 10;
|
||
originalPriceWithTax = new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(originalPrice);
|
||
}
|
||
|
||
return (
|
||
<Container
|
||
maxWidth="lg"
|
||
sx={{
|
||
p: { xs: 2, md: 2 },
|
||
pb: { xs: 4, md: 8 },
|
||
flexGrow: 1
|
||
}}
|
||
>
|
||
{/* Breadcrumbs */}
|
||
<Box
|
||
sx={{
|
||
mb: 2,
|
||
position: ["-webkit-sticky", "sticky"], // Provide both prefixed and standard
|
||
top: {
|
||
xs: "110px",
|
||
sm: "110px",
|
||
md: "110px",
|
||
lg: "110px",
|
||
} /* Offset to sit below the header 120 mith menu for md and lg*/,
|
||
left: 0,
|
||
width: "100%",
|
||
display: "flex",
|
||
zIndex: (theme) =>
|
||
theme.zIndex.appBar - 1 /* Just below the AppBar */,
|
||
py: 0,
|
||
px: 2,
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
ml: { xs: 0, md: 0 },
|
||
display: "inline-flex",
|
||
px: 0,
|
||
py: 1,
|
||
backgroundColor: "#2e7d32", //primary dark green
|
||
borderRadius: 1,
|
||
}}
|
||
>
|
||
<Typography variant="body2" color="text.secondary">
|
||
<Link
|
||
to="/"
|
||
onClick={() => this.props.navigate(-1)}
|
||
style={{
|
||
paddingLeft: 16,
|
||
paddingRight: 16,
|
||
paddingTop: 8,
|
||
paddingBottom: 8,
|
||
textDecoration: "none",
|
||
color: "#fff",
|
||
fontWeight: "bold",
|
||
}}
|
||
>
|
||
{this.props.t ? this.props.t('common.back') : 'Zurück'}
|
||
</Link>
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: { xs: "column", md: "row" },
|
||
gap: 4,
|
||
background: "#fff",
|
||
borderRadius: 2,
|
||
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
||
}}
|
||
>
|
||
<ProductImage
|
||
product={product}
|
||
fullscreenOpen={this.state.imageDialogOpen}
|
||
onOpenFullscreen={this.handleOpenDialog}
|
||
onCloseFullscreen={this.handleCloseDialog}
|
||
/>
|
||
|
||
{/* Product Details */}
|
||
<Box
|
||
sx={{
|
||
flex: "1 1 60%",
|
||
p: { xs: 2, md: 4 },
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
{/* Product identifiers */}
|
||
<Box sx={{ mb: 1 }}>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{this.props.t ? this.props.t('product.articleNumber') : 'Artikelnummer'}: {product.articleNumber} {product.gtin ? ` | GTIN: ${product.gtin}` : ""}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* Product title */}
|
||
<Typography
|
||
variant="h4"
|
||
component="h1"
|
||
gutterBottom
|
||
sx={{ fontWeight: 600, color: "#333" }}
|
||
>
|
||
{cleanProductName(product.name)}
|
||
</Typography>
|
||
|
||
{/* Manufacturer if available */}
|
||
{product.manufacturer && (
|
||
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
|
||
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
|
||
{this.props.t ? this.props.t('product.manufacturer') : 'Hersteller'}:
|
||
<Button
|
||
variant="text"
|
||
size="small"
|
||
onClick={() => {
|
||
if (this.props.navigate) {
|
||
this.props.navigate(`/search?q=${encodeURIComponent(product.manufacturer)}`);
|
||
}
|
||
}}
|
||
sx={{
|
||
ml: 0.5,
|
||
p: 0,
|
||
minWidth: 'auto',
|
||
height: 'auto',
|
||
fontSize: 'inherit',
|
||
fontStyle: 'inherit',
|
||
textTransform: 'none',
|
||
'&:hover': {
|
||
backgroundColor: 'transparent',
|
||
textDecoration: 'underline',
|
||
}
|
||
}}
|
||
>
|
||
{product.manufacturer}
|
||
</Button>
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Attribute images and chips with action buttons */}
|
||
|
||
<Box sx={{ minHeight: "107px", display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2, gap: 2 }}>
|
||
{(attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert])) && (
|
||
<Stack direction="row" spacing={0} sx={{ flexWrap: "wrap", gap: 1, flex: 1 }}>
|
||
{attributes
|
||
.filter(attribute => attributeImages[attribute.kMerkmalWert])
|
||
.map((attribute) => {
|
||
const key = attribute.kMerkmalWert;
|
||
return (
|
||
<Box key={key} sx={{ mb: 1,border: "1px solid #e0e0e0", borderRadius: 1 }}>
|
||
<CardMedia
|
||
component="img"
|
||
style={{ width: "72px", height: "98px" }}
|
||
image={attributeImages[key]}
|
||
alt={`Attribute ${key}`}
|
||
/>
|
||
</Box>
|
||
);
|
||
})}
|
||
{attributes
|
||
.filter(attribute => !attributeImages[attribute.kMerkmalWert])
|
||
.map((attribute) => (
|
||
<Chip
|
||
key={attribute.kMerkmalWert}
|
||
label={attribute.cWert}
|
||
disabled
|
||
sx={{
|
||
'&.Mui-disabled': {
|
||
opacity: 1, // ← Remove the "fog"
|
||
},
|
||
'& .MuiChip-label': {
|
||
fontWeight: 'bold',
|
||
color: 'inherit', // ← Keep normal text color
|
||
},
|
||
}}
|
||
/>
|
||
))}
|
||
</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 */}
|
||
{product.weight > 0 && (
|
||
<Box sx={{ mb: 2 }}>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{this.props.t ? this.props.t('product.weight', { weight: product.weight.toFixed(1).replace(".", ",") }) : `Gewicht: ${product.weight.toFixed(1).replace(".", ",")} kg`}
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Price and availability section */}
|
||
<Box
|
||
sx={{
|
||
mt: "auto",
|
||
p: 3,
|
||
background: "#f9f9f9",
|
||
borderRadius: 2,
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: { xs: "column", sm: "row" },
|
||
justifyContent: "space-between",
|
||
alignItems: { xs: "flex-start", sm: "flex-start" },
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Box>
|
||
<Typography
|
||
variant="h5"
|
||
color="primary"
|
||
sx={{ fontWeight: "bold" }}
|
||
>
|
||
{originalPriceWithTax && (
|
||
<Typography
|
||
component="span"
|
||
variant="h6"
|
||
sx={{
|
||
textDecoration: 'line-through',
|
||
color: 'text.secondary',
|
||
mr: 1,
|
||
display: 'inline-block'
|
||
}}
|
||
>
|
||
{originalPriceWithTax}
|
||
</Typography>
|
||
)}
|
||
{priceWithTax}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{this.props.t ? this.props.t('product.inclVat', { vat: product.vat }) : `inkl. ${product.vat}% MwSt.`}
|
||
{product.cGrundEinheit && product.fGrundPreis && (
|
||
<>; {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(product.fGrundPreis)}/{product.cGrundEinheit}</>
|
||
)}
|
||
</Typography>
|
||
|
||
{product.versandklasse &&
|
||
product.versandklasse != "standard" &&
|
||
product.versandklasse != "kostenlos" && (
|
||
<Typography variant="body2" color="text.secondary">
|
||
{product.versandklasse}
|
||
</Typography>
|
||
)}
|
||
</Box>
|
||
|
||
{/* Savings comparison - positioned between price and cart button */}
|
||
{product.komponenten && komponentenLoaded && totalKomponentenPrice > product.price &&
|
||
(totalKomponentenPrice - product.price >= 2 &&
|
||
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
|
||
<Box sx={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
minWidth: { xs: "100%", sm: "200px" }
|
||
}}>
|
||
<Box sx={{ p: 2, borderRadius: 1, backgroundColor: "#e8f5e8", textAlign: "center" }}>
|
||
<Typography
|
||
variant="body2"
|
||
sx={{
|
||
fontWeight: "bold",
|
||
color: "success.main"
|
||
}}
|
||
>
|
||
{this.props.t ? this.props.t('product.youSave', {
|
||
amount: new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(totalKomponentenPrice - product.price)
|
||
}) : `Sie sparen: ${new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(totalKomponentenPrice - product.price)}`}
|
||
</Typography>
|
||
<Typography variant="caption" color="text.secondary">
|
||
{this.props.t ? this.props.t('product.cheaperThanIndividual') : 'Günstiger als Einzelkauf'}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: { xs: "column", sm: "row" },
|
||
gap: 2,
|
||
alignItems: "flex-start",
|
||
}}
|
||
>
|
||
{isSteckling && product.available == 1 && (
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
<AddToCartButton
|
||
steckling={true}
|
||
cartButton={true}
|
||
seoName={product.seoName}
|
||
pictureList={product.pictureList}
|
||
available={product.available}
|
||
id={product.id + "steckling"}
|
||
price={0}
|
||
vat={product.vat}
|
||
weight={product.weight}
|
||
availableSupplier={product.availableSupplier}
|
||
komponenten={product.komponenten}
|
||
name={cleanProductName(product.name) + " Stecklingsvorbestellung 1 Stück"}
|
||
versandklasse={"nur Abholung"}
|
||
/>
|
||
<Typography
|
||
variant="caption"
|
||
sx={{
|
||
fontStyle: "italic",
|
||
color: "text.secondary",
|
||
textAlign: "center",
|
||
mt: 1
|
||
}}
|
||
>
|
||
{this.props.t ? this.props.t('product.pickupPrice') : 'Abholpreis: 19,90 € pro Steckling.'}
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
<AddToCartButton
|
||
cartButton={true}
|
||
seoName={product.seoName}
|
||
pictureList={product.pictureList}
|
||
available={product.available}
|
||
id={product.id}
|
||
availableSupplier={product.availableSupplier}
|
||
komponenten={product.komponenten}
|
||
cGrundEinheit={product.cGrundEinheit}
|
||
fGrundPreis={product.fGrundPreis}
|
||
price={product.price}
|
||
vat={product.vat}
|
||
weight={product.weight}
|
||
name={cleanProductName(product.name)}
|
||
versandklasse={product.versandklasse}
|
||
/>
|
||
|
||
<Typography
|
||
variant="caption"
|
||
sx={{
|
||
fontStyle: "italic",
|
||
color: "text.secondary",
|
||
textAlign: "center",
|
||
mt: 1
|
||
}}
|
||
>
|
||
{product.id.toString().endsWith("steckling") ?
|
||
(this.props.t ? this.props.t('delivery.times.cutting14Days') : "Lieferzeit: 14 Tage") :
|
||
product.available == 1 ?
|
||
(this.props.t ? this.props.t('delivery.times.standard2to3Days') : "Lieferzeit: 2-3 Tage") :
|
||
product.availableSupplier == 1 ?
|
||
(this.props.t ? this.props.t('delivery.times.supplier7to9Days') : "Lieferzeit: 7-9 Tage") : ""}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Product full description */}
|
||
{(product.description || upgrading) && (
|
||
<Box
|
||
sx={{
|
||
mt: 4,
|
||
p: 4,
|
||
background: "#fff",
|
||
borderRadius: 2,
|
||
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
||
position: "relative",
|
||
}}
|
||
>
|
||
{/* Share button */}
|
||
<Button
|
||
onClick={this.handleShareClick}
|
||
startIcon={<ShareIcon fontSize="small" />}
|
||
sx={{
|
||
position: "absolute",
|
||
top: 8,
|
||
right: 8,
|
||
backgroundColor: "#f5f5f5",
|
||
"&:hover": {
|
||
backgroundColor: "#e0e0e0",
|
||
},
|
||
zIndex: 1,
|
||
fontSize: "0.75rem",
|
||
px: 1.5,
|
||
py: 0.5,
|
||
minWidth: "auto",
|
||
textTransform: "none",
|
||
}}
|
||
size="small"
|
||
>
|
||
Teilen
|
||
</Button>
|
||
<Box
|
||
sx={{
|
||
mt: 2,
|
||
lineHeight: 1.7,
|
||
"& p": { mt: 0, mb: 2 },
|
||
"& strong": { fontWeight: 600 },
|
||
}}
|
||
>
|
||
{product.description ? (() => {
|
||
try {
|
||
// Sanitize HTML to remove invalid tags, but preserve style attributes and <product> tags
|
||
const sanitized = sanitizeHtml(product.description, {
|
||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'product']),
|
||
allowedAttributes: {
|
||
'*': ['class', 'style'],
|
||
'a': ['href', 'title'],
|
||
'img': ['src', 'alt', 'width', 'height'],
|
||
'product': ['articlenr']
|
||
},
|
||
disallowedTagsMode: 'discard'
|
||
});
|
||
|
||
// Parse with custom replace function to handle <product> tags
|
||
return parse(sanitized, {
|
||
replace: (domNode) => {
|
||
if (domNode.type === 'tag' && domNode.name === 'product') {
|
||
const articleNr = domNode.attribs && domNode.attribs['articlenr'];
|
||
if (articleNr) {
|
||
// Render embedded product component
|
||
return this.renderEmbeddedProduct(articleNr);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.warn('Failed to parse product description HTML:', error);
|
||
// Fallback to rendering as plain text if HTML parsing fails
|
||
return <span>{product.description}</span>;
|
||
}
|
||
})() : upgrading ? (
|
||
<Box sx={{ textAlign: "center", py: 2 }}>
|
||
<Typography variant="body1" color="text.secondary">
|
||
{this.props.t ? this.props.t('product.loadingDescription') : 'Produktbeschreibung wird geladen...'}
|
||
</Typography>
|
||
</Box>
|
||
) : null}
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Share Popper */}
|
||
<Popper
|
||
open={sharePopperOpen}
|
||
anchorEl={shareAnchorEl}
|
||
placement="bottom-end"
|
||
sx={{ zIndex: 1300 }}
|
||
>
|
||
<ClickAwayListener onClickAway={this.handleShareClose}>
|
||
<Box
|
||
sx={{
|
||
bgcolor: "background.paper",
|
||
border: "1px solid #ccc",
|
||
borderRadius: 1,
|
||
boxShadow: "0 4px 8px rgba(0,0,0,0.1)",
|
||
minWidth: 200,
|
||
}}
|
||
>
|
||
<MenuList>
|
||
<MenuItem onClick={this.handleEmbedShare}>
|
||
<ListItemIcon>
|
||
<CodeIcon fontSize="small" />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Einbetten" />
|
||
</MenuItem>
|
||
<MenuItem onClick={this.handleWhatsAppShare}>
|
||
<ListItemIcon>
|
||
<WhatsAppIcon fontSize="small" sx={{ color: "#25D366" }} />
|
||
</ListItemIcon>
|
||
<ListItemText primary="WhatsApp" />
|
||
</MenuItem>
|
||
<MenuItem onClick={this.handleTelegramShare}>
|
||
<ListItemIcon>
|
||
<TelegramIcon fontSize="small" sx={{ color: "#0088CC" }} />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Telegram" />
|
||
</MenuItem>
|
||
<MenuItem onClick={this.handleFacebookShare}>
|
||
<ListItemIcon>
|
||
<FacebookIcon fontSize="small" sx={{ color: "#1877F2" }} />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Facebook" />
|
||
</MenuItem>
|
||
<MenuItem onClick={this.handleEmailShare}>
|
||
<ListItemIcon>
|
||
<EmailIcon fontSize="small" />
|
||
</ListItemIcon>
|
||
<ListItemText primary="E-Mail" />
|
||
</MenuItem>
|
||
<MenuItem onClick={this.handleLinkCopy}>
|
||
<ListItemIcon>
|
||
<LinkIcon fontSize="small" />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Link kopieren" />
|
||
</MenuItem>
|
||
</MenuList>
|
||
</Box>
|
||
</ClickAwayListener>
|
||
</Popper>
|
||
|
||
{/* Article Question Form */}
|
||
<Collapse in={this.state.showQuestionForm}>
|
||
<div id="question-form">
|
||
<ArticleQuestionForm
|
||
productId={product.id}
|
||
productName={cleanProductName(product.name)}
|
||
/>
|
||
</div>
|
||
</Collapse>
|
||
|
||
{/* Article Rating Form */}
|
||
<Collapse in={this.state.showRatingForm}>
|
||
<div id="rating-form">
|
||
<ArticleRatingForm
|
||
productId={product.id}
|
||
productName={cleanProductName(product.name)}
|
||
/>
|
||
</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)}
|
||
/>
|
||
</Collapse>
|
||
)}
|
||
|
||
{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)" }}>
|
||
<Typography variant="h2" component="h2" gutterBottom>{this.props.t ? this.props.t('product.consistsOf') : 'Bestehend aus:'}</Typography>
|
||
<Box sx={{ maxWidth: 800, mx: "auto" }}>
|
||
|
||
{(console.log("komponentenLoaded:", komponentenLoaded), komponentenLoaded) ? (
|
||
<>
|
||
{console.log("Rendering loaded komponenten:", this.state.komponenten.length, "komponentenData:", Object.keys(komponentenData).length)}
|
||
{this.state.komponenten.map((komponent, index) => {
|
||
const komponentData = komponentenData[komponent.id];
|
||
console.log(`Rendering komponent ${komponent.id}:`, komponentData);
|
||
|
||
// Don't show border on last item (pricing section has its own top border)
|
||
const isLastItem = index === this.state.komponenten.length - 1;
|
||
const showBorder = !isLastItem;
|
||
|
||
if (!komponentData || !komponentData.loaded) {
|
||
return (
|
||
<Box key={komponent.id} sx={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
py: 1,
|
||
borderBottom: showBorder ? "1px solid #eee" : "none",
|
||
minHeight: "70px" // Consistent height to prevent layout shifts
|
||
}}>
|
||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||
{/* Empty placeholder for image */}
|
||
</Box>
|
||
<Box>
|
||
<Typography variant="body1">
|
||
{index + 1}. Lädt...
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{komponent.count}x
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||
-
|
||
</Typography>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
const itemPrice = komponentData.price * parseInt(komponent.count);
|
||
const formattedPrice = new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(itemPrice);
|
||
|
||
return (
|
||
<Box
|
||
key={komponent.id}
|
||
component={Link}
|
||
to={`/Artikel/${komponentData.seoName}`}
|
||
sx={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
py: 1,
|
||
borderBottom: showBorder ? "1px solid #eee" : "none",
|
||
textDecoration: "none",
|
||
color: "inherit",
|
||
minHeight: "70px", // Consistent height to prevent layout shifts
|
||
"&:hover": {
|
||
backgroundColor: "#f5f5f5"
|
||
}
|
||
}}
|
||
>
|
||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||
<Box sx={{ width: 50, height: 50, flexShrink: 0 }}>
|
||
{komponentenImages[komponent.id] ? (
|
||
<CardMedia
|
||
component="img"
|
||
height="50"
|
||
image={komponentenImages[komponent.id]}
|
||
alt={komponentData.name}
|
||
sx={{
|
||
objectFit: "contain",
|
||
borderRadius: 1,
|
||
border: "1px solid #e0e0e0"
|
||
}}
|
||
/>
|
||
) : (
|
||
<CardMedia
|
||
component="img"
|
||
height="50"
|
||
image="/assets/images/nopicture.jpg"
|
||
alt={komponentData.name}
|
||
sx={{
|
||
objectFit: "contain",
|
||
borderRadius: 1,
|
||
border: "1px solid #e0e0e0"
|
||
}}
|
||
/>
|
||
)}
|
||
</Box>
|
||
<Box>
|
||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||
{index + 1}. {cleanProductName(komponentData.name)}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{komponent.count}x à {new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(komponentData.price)}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||
{formattedPrice}
|
||
</Typography>
|
||
</Box>
|
||
);
|
||
})}
|
||
|
||
{/* Total price and savings display - only show when prices differ meaningfully */}
|
||
{totalKomponentenPrice > product.price &&
|
||
(totalKomponentenPrice - product.price >= 2 &&
|
||
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
|
||
<Box sx={{ mt: 3, pt: 2, borderTop: "2px solid #eee" }}>
|
||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
|
||
<Typography variant="h6">
|
||
{this.props.t ? this.props.t('product.individualPriceTotal') : 'Einzelpreis gesamt:'}
|
||
</Typography>
|
||
<Typography variant="h6" sx={{ textDecoration: "line-through", color: "text.secondary" }}>
|
||
{new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(totalKomponentenPrice)}
|
||
</Typography>
|
||
</Box>
|
||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
|
||
<Typography variant="h6">
|
||
{this.props.t ? this.props.t('product.setPrice') : 'Set-Preis:'}
|
||
</Typography>
|
||
<Typography variant="h6" color="primary" sx={{ fontWeight: "bold" }}>
|
||
{new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(product.price)}
|
||
</Typography>
|
||
</Box>
|
||
{totalSavings > 0 && (
|
||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mt: 2, p: 2, backgroundColor: "#e8f5e8", borderRadius: 1 }}>
|
||
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
|
||
{this.props.t ? this.props.t('product.yourSavings') : 'Ihre Ersparnis:'}
|
||
</Typography>
|
||
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
|
||
{new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(totalSavings)}
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
)}
|
||
</>
|
||
) : (
|
||
// Loading state
|
||
<Box>
|
||
{this.state.komponenten.map((komponent, index) => {
|
||
// For loading state, we don't know if pricing will be shown, so show all borders
|
||
return (
|
||
<Box key={komponent.id} sx={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
py: 1,
|
||
borderBottom: "1px solid #eee",
|
||
minHeight: "70px" // Consistent height to prevent layout shifts
|
||
}}>
|
||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||
{/* Empty placeholder for image */}
|
||
</Box>
|
||
<Box>
|
||
<Typography variant="body1">
|
||
{this.props.t ? this.props.t('product.loadingComponentDetails', { index: index + 1 }) : `${index + 1}. Lädt Komponent-Details...`}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{komponent.count}x
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||
-
|
||
</Typography>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Similar Products Section */}
|
||
{this.state.similarProducts && this.state.similarProducts.length > 0 && (
|
||
<Box sx={{ mt: 4, p: 4, background: "#fff", borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
|
||
<Typography variant="h4" component="h2" gutterBottom sx={{ mb: 3 }}>
|
||
{this.props.t ? this.props.t('product.similarProducts') : 'Ähnliche Produkte'}
|
||
</Typography>
|
||
<Box sx={{
|
||
display: "grid",
|
||
gridTemplateColumns: {
|
||
xs: "1fr",
|
||
sm: "repeat(2, 1fr)",
|
||
md: "repeat(3, 1fr)",
|
||
lg: "repeat(4, 1fr)"
|
||
},
|
||
gap: 2
|
||
}}>
|
||
{this.state.similarProducts.map((similarProductData, index) => {
|
||
const product = similarProductData.product;
|
||
return (
|
||
<Box key={product.id} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||
<Product
|
||
id={product.id}
|
||
name={product.name}
|
||
seoName={product.seoName}
|
||
price={product.price}
|
||
currency={product.currency}
|
||
available={product.available}
|
||
manufacturer={product.manufacturer}
|
||
vat={product.vat}
|
||
cGrundEinheit={product.cGrundEinheit}
|
||
fGrundPreis={product.fGrundPreis}
|
||
incoming={product.incomingDate}
|
||
neu={product.neu}
|
||
thc={product.thc}
|
||
floweringWeeks={product.floweringWeeks}
|
||
versandklasse={product.versandklasse}
|
||
weight={product.weight}
|
||
pictureList={product.pictureList}
|
||
availableSupplier={product.availableSupplier}
|
||
komponenten={product.komponenten}
|
||
rebate={product.rebate}
|
||
categoryId={product.kategorien ? product.kategorien.split(',')[0] : undefined}
|
||
priority={index < 6 ? 'high' : 'auto'}
|
||
t={this.props.t}
|
||
/>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Snackbar for user feedback */}
|
||
<Snackbar
|
||
open={snackbarOpen}
|
||
autoHideDuration={4000}
|
||
onClose={this.handleSnackbarClose}
|
||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||
>
|
||
<Alert
|
||
onClose={this.handleSnackbarClose}
|
||
severity={snackbarSeverity}
|
||
sx={{ width: '100%' }}
|
||
>
|
||
{snackbarMessage}
|
||
</Alert>
|
||
</Snackbar>
|
||
</Container>
|
||
);
|
||
}
|
||
}
|
||
|
||
export default withI18n()(ProductDetailPage);
|
||
|