import React, { Component } from "react"; import { Box, Typography, CardMedia, Stack, Chip, Button, Collapse } from "@mui/material"; import { Link } from "react-router-dom"; import parse from "html-react-parser"; import AddToCartButton from "./AddToCartButton.js"; import Images from "./Images.js"; import { withI18n } from "../i18n/withTranslation.js"; import ArticleQuestionForm from "./ArticleQuestionForm.js"; import ArticleRatingForm from "./ArticleRatingForm.js"; import ArticleAvailabilityForm from "./ArticleAvailabilityForm.js"; // Utility function to clean product names by removing trailing number in parentheses const cleanProductName = (name) => { 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); if ( window.productDetailCache && window.productDetailCache[this.props.seoName] ) { this.state = { product: window.productDetailCache[this.props.seoName], loading: 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 }; } else { this.state = { product: null, loading: true, 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() { this.loadProductData(); } componentDidUpdate(prevProps) { if (prevProps.seoName !== this.props.seoName) this.setState( { product: null, loading: true, error: null, imageDialogOpen: false }, this.loadProductData ); // Handle socket connection changes const wasConnected = prevProps.socket && prevProps.socket.connected; const isNowConnected = this.props.socket && this.props.socket.connected; if (!wasConnected && isNowConnected && this.state.loading) { // Socket just connected and we're still loading, retry loading data 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; } // Check if socketB is available if (!this.props.socketB || !this.props.socketB.connected) { console.log("SocketB not connected yet, skipping image load for komponent:", komponentId); return; } // Fetch image from server this.props.socketB.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; } // If not cached, fetch from server (similar to loadProductData) if (!this.props.socket || !this.props.socket.connected) { console.log("Socket not connected yet, waiting for connection to load komponent data"); return; } // Mark this komponent as loading this.setState(prevState => ({ komponentenData: { ...prevState.komponentenData, [id]: { ...prevState.komponentenData[id], loading: true, loaded: false, count: parseInt(count) } } })); this.props.socket.emit( "getProductView", { articleId: id }, (res) => { if (res.success) { // Cache the successful response window.productDetailCache[id] = res.product; // Load komponent image if available if (res.product.pictureList) { this.loadKomponentImage(id, res.product.pictureList); } // Update state with loaded data this.setState(prevState => { const newKomponentenData = { ...prevState.komponentenData, [id]: { ...res.product, 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 = () => { if (!this.props.socket || !this.props.socket.connected) { // Socket not connected yet, but don't show error immediately on first load // The componentDidUpdate will retry when socket connects console.log("Socket not connected yet, waiting for connection to load product data"); return; } this.props.socket.emit( "getProductView", { seoName: this.props.seoName }, (res) => { if (res.success) { res.product.seoName = this.props.seoName; // Initialize cache if it doesn't exist if (!window.productDetailCache) { window.productDetailCache = {}; } // Cache the product data window.productDetailCache[this.props.seoName] = res.product; const komponenten = []; if(res.product.komponenten) { for(const komponent of res.product.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: res.product, loading: false, 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 if (this.props.socketB && this.props.socketB.connected) { this.props.socketB.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, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } = this.state; if (loading) { return ( Produkt wird geladen... ); } 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); 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.pictureList && ( )} {product.pictureList && ( )} {/* 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'}: {product.manufacturer} )} {/* 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 */} {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 && ( {parse(product.description)} )} {/* 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 - ); })} )} )}
); } } export default withI18n()(ProductDetailPage);