import React, { Component } from "react"; import { Box, Container, Typography, CardMedia, Stack, Chip, Button, Collapse } from "@mui/material"; import { Link } from "react-router-dom"; import parse from "html-react-parser"; import AddToCartButton from "./AddToCartButton.js"; import ProductImage from "./ProductImage.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; if (window.productDetailCache && window.productDetailCache[this.props.seoName]) { cachedData = window.productDetailCache[this.props.seoName]; } 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.seoName === this.props.seoName) { partialProduct = cached; 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 }; } 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 }; } 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 }; } } 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 komponenten if they exist 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 }, 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 }, 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 = {}; } // Check if this komponent is already cached if (window.productDetailCache[id]) { const cachedProduct = window.productDetailCache[id]; // 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); const currentLanguage = this.props.languageContext?.currentLanguage const currentLanguage2 = this.props.i18n?.language || 'de'; console.log('debuglanguage', currentLanguage, currentLanguage2); 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[id] = 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 }; }); } } ); } loadProductData = () => { console.log('loadProductData', this.props.seoName); const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de'; console.log('debuglanguage', 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[this.props.seoName] = 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 }, () => { if(komponenten.length > 0) { for(const komponent of komponenten) { this.loadKomponent(komponent.id, komponent.count); } } }); console.log("getProductView", res); // Initialize window-level attribute image cache if it doesn't exist if (!window.attributeImageCache) { window.attributeImageCache = {}; } if (res.attributes && res.attributes.length > 0) { const attributeImages = {}; for (const attribute of res.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 }); } } } 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' }); } }; render() { const { product, loading, upgrading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } = 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 (