diff --git a/src/components/ProductDetailPage.js b/src/components/ProductDetailPage.js index 95dbbca..81e9dae 100644 --- a/src/components/ProductDetailPage.js +++ b/src/components/ProductDetailPage.js @@ -94,6 +94,9 @@ class ProductDetailPage extends Component { 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, @@ -143,6 +146,9 @@ class ProductDetailPage extends Component { 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, @@ -174,6 +180,9 @@ class ProductDetailPage extends Component { 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, @@ -797,6 +806,234 @@ class ProductDetailPage extends Component { 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 + } + }; + }); + } + }); + }; + render() { const { product, loading, upgrading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings, shareAnchorEl, sharePopperOpen, snackbarOpen, snackbarMessage, snackbarSeverity } = this.state; @@ -1335,16 +1572,30 @@ class ProductDetailPage extends Component { > {product.description ? (() => { try { - // Sanitize HTML to remove invalid tags, but preserve style attributes - return parse(sanitizeHtml(product.description, { - allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), + // 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'] + '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 diff --git a/src/i18n/locales/de/product.js b/src/i18n/locales/de/product.js index 904e23e..c40d047 100644 --- a/src/i18n/locales/de/product.js +++ b/src/i18n/locales/de/product.js @@ -25,6 +25,7 @@ export default { "pickupPrice": "Abholpreis: 19,90 € pro Steckling.", "consistsOf": "Bestehend aus:", "loadingComponentDetails": "{{index}}. Lädt Komponent-Details...", + "loadingProduct": "Produkt wird geladen...", "individualPriceTotal": "Einzelpreis gesamt:", "setPrice": "Set-Preis:", "yourSavings": "Ihre Ersparnis:", diff --git a/src/i18n/locales/en/product.js b/src/i18n/locales/en/product.js index 0da9d34..8ce7ad1 100644 --- a/src/i18n/locales/en/product.js +++ b/src/i18n/locales/en/product.js @@ -25,6 +25,7 @@ export default { "pickupPrice": "Pickup price: €19.90 per cutting.", // Abholpreis: 19,90 € pro Steckling. "consistsOf": "Consists of:", // Bestehend aus: "loadingComponentDetails": "{{index}}. Loading component details...", // {{index}}. Lädt Komponent-Details... + "loadingProduct": "Product is loading...", // Produkt wird geladen... "individualPriceTotal": "Total individual price:", // Einzelpreis gesamt: "setPrice": "Set price:", // Set-Preis: "yourSavings": "Your savings:", // Ihre Ersparnis: