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, // 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, // 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, // 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() { // 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) { 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 || [] }, () => { 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() { 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 loading message if no prerender content return (