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 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 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 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/jpeg' })); // 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/jpeg", }); 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 = ``; 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 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 ( Produkt nicht gefunden (Artikelnr: {articleNr}) ); } 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 ( {/* Empty placeholder for image */} {this.props.t('product.loadingProduct')} {this.props.t('product.articleNumber')}: {articleNr} ); } // Product data is loaded, render it const embeddedImages = this.state.embeddedProductImages || {}; const productImage = embeddedImages[articleNr]; return ( {productImage ? ( ) : ( )} {cleanProductName(productData.name)} {this.props.t('product.articleNumber')}: {productData.articleNumber} {new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR", }).format(productData.price)} inkl. MwSt. ); }; // 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/jpeg' })); 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 (
); } // Fallback to blank page if no prerender content return
; } if (error) { return ( Fehler {error} {this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'} ); } if (!product) { return ( Produkt nicht gefunden Das gesuchte Produkt existiert nicht oder wurde entfernt. {this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'} ); } // 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 ( {/* Breadcrumbs */} theme.zIndex.appBar - 1 /* Just below the AppBar */, py: 0, px: 2, }} > 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'} {/* Product Details */} {/* Product identifiers */} {this.props.t ? this.props.t('product.articleNumber') : 'Artikelnummer'}: {product.articleNumber} {product.gtin ? ` | GTIN: ${product.gtin}` : ""} {/* Product title */} {cleanProductName(product.name)} {/* Manufacturer if available */} {product.manufacturer && ( {this.props.t ? this.props.t('product.manufacturer') : 'Hersteller'}: )} {/* 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) => ( ))} {/* Right-aligned action buttons */} {(product.available !== 1 && product.availableSupplier !== 1) && ( )} )} {/* Weight */} {product.weight > 0 && ( {this.props.t ? this.props.t('product.weight', { weight: product.weight.toFixed(1).replace(".", ",") }) : `Gewicht: ${product.weight.toFixed(1).replace(".", ",")} kg`} )} {/* Price and availability section */} {originalPriceWithTax && ( {originalPriceWithTax} )} {priceWithTax} {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} )} {product.versandklasse && product.versandklasse != "standard" && product.versandklasse != "kostenlos" && ( {product.versandklasse} )} {/* 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) && ( {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)}`} {this.props.t ? this.props.t('product.cheaperThanIndividual') : 'Günstiger als Einzelkauf'} )} {isSteckling && product.available == 1 && ( {this.props.t ? this.props.t('product.pickupPrice') : 'Abholpreis: 19,90 € pro Steckling.'} )} {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") : ""} {/* Product full description */} {(product.description || upgrading) && ( {/* Share button */} {product.description ? (() => { try { // Sanitize HTML to remove invalid tags, but preserve style attributes and 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 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 {product.description}; } })() : upgrading ? ( {this.props.t ? this.props.t('product.loadingDescription') : 'Produktbeschreibung wird geladen...'} ) : null} )} {/* Share Popper */} {/* 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:'} {(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 ( {/* Empty placeholder for image */} {index + 1}. Lädt... {komponent.count}x - ); } const itemPrice = komponentData.price * parseInt(komponent.count); const formattedPrice = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR", }).format(itemPrice); return ( {komponentenImages[komponent.id] ? ( ) : ( )} {index + 1}. {cleanProductName(komponentData.name)} {komponent.count}x à {new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR", }).format(komponentData.price)} {formattedPrice} ); })} {/* 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) && ( {this.props.t ? this.props.t('product.individualPriceTotal') : 'Einzelpreis gesamt:'} {new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR", }).format(totalKomponentenPrice)} {this.props.t ? this.props.t('product.setPrice') : 'Set-Preis:'} {new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR", }).format(product.price)} {totalSavings > 0 && ( {this.props.t ? this.props.t('product.yourSavings') : 'Ihre Ersparnis:'} {new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR", }).format(totalSavings)} )} )} ) : ( // Loading state {this.state.komponenten.map((komponent, index) => { // For loading state, we don't know if pricing will be shown, so show all borders return ( {/* Empty placeholder for image */} {this.props.t ? this.props.t('product.loadingComponentDetails', { index: index + 1 }) : `${index + 1}. Lädt Komponent-Details...`} {komponent.count}x - ); })} )} )} {/* Similar Products Section */} {this.state.similarProducts && this.state.similarProducts.length > 0 && ( {this.props.t ? this.props.t('product.similarProducts') : 'Ähnliche Produkte'} {this.state.similarProducts.map((similarProductData, index) => { const product = similarProductData.product; return ( ); })} )} {/* Snackbar for user feedback */} {snackbarMessage}
); } } export default withI18n()(ProductDetailPage);