|
|
|
|
@@ -94,6 +94,9 @@ class ProductDetailPage extends Component {
|
|
|
|
|
komponentenImages: {}, // Store tiny pictures for komponenten
|
|
|
|
|
totalKomponentenPrice: 0,
|
|
|
|
|
totalSavings: 0,
|
|
|
|
|
// Embedded products from <product> 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 <product> 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 <product> 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 <product articlenr="..."> 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 (
|
|
|
|
|
<Box
|
|
|
|
|
key={`embedded-${articleNr}`}
|
|
|
|
|
sx={{
|
|
|
|
|
my: 2,
|
|
|
|
|
p: 2,
|
|
|
|
|
background: "#fff3f3",
|
|
|
|
|
borderRadius: 2,
|
|
|
|
|
border: "1px solid #ffcdd2"
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Typography variant="body2" color="error">
|
|
|
|
|
Produkt nicht gefunden (Artikelnr: {articleNr})
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
<Box
|
|
|
|
|
key={`embedded-${articleNr}`}
|
|
|
|
|
sx={{
|
|
|
|
|
my: 2,
|
|
|
|
|
p: 2,
|
|
|
|
|
background: "#f9f9f9",
|
|
|
|
|
borderRadius: 2,
|
|
|
|
|
border: "1px solid #e0e0e0"
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
|
|
|
|
<Box sx={{ width: 60, height: 60, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
|
|
|
|
{/* Empty placeholder for image */}
|
|
|
|
|
</Box>
|
|
|
|
|
<Box>
|
|
|
|
|
<Typography variant="body1">
|
|
|
|
|
{this.props.t('product.loadingProduct')}
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="body2" color="text.secondary">
|
|
|
|
|
{this.props.t('product.articleNumber')}: {articleNr}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Product data is loaded, render it
|
|
|
|
|
const embeddedImages = this.state.embeddedProductImages || {};
|
|
|
|
|
const productImage = embeddedImages[articleNr];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box
|
|
|
|
|
key={`embedded-${articleNr}`}
|
|
|
|
|
component={Link}
|
|
|
|
|
to={`/Artikel/${productData.seoName}`}
|
|
|
|
|
sx={{
|
|
|
|
|
display: "block",
|
|
|
|
|
my: 2,
|
|
|
|
|
p: 2,
|
|
|
|
|
background: "#f9f9f9",
|
|
|
|
|
borderRadius: 2,
|
|
|
|
|
border: "1px solid #e0e0e0",
|
|
|
|
|
textDecoration: "none",
|
|
|
|
|
color: "inherit",
|
|
|
|
|
transition: "all 0.2s",
|
|
|
|
|
"&:hover": {
|
|
|
|
|
backgroundColor: "#f0f0f0",
|
|
|
|
|
boxShadow: "0 2px 8px rgba(0,0,0,0.1)"
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
|
|
|
|
<Box sx={{ width: 60, height: 60, flexShrink: 0 }}>
|
|
|
|
|
{productImage ? (
|
|
|
|
|
<CardMedia
|
|
|
|
|
component="img"
|
|
|
|
|
height="60"
|
|
|
|
|
image={productImage}
|
|
|
|
|
alt={productData.name}
|
|
|
|
|
sx={{
|
|
|
|
|
objectFit: "contain",
|
|
|
|
|
borderRadius: 1,
|
|
|
|
|
border: "1px solid #e0e0e0"
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<CardMedia
|
|
|
|
|
component="img"
|
|
|
|
|
height="60"
|
|
|
|
|
image="/assets/images/nopicture.jpg"
|
|
|
|
|
alt={productData.name}
|
|
|
|
|
sx={{
|
|
|
|
|
objectFit: "contain",
|
|
|
|
|
borderRadius: 1,
|
|
|
|
|
border: "1px solid #e0e0e0"
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ flex: 1 }}>
|
|
|
|
|
<Typography variant="body1" sx={{ fontWeight: 500, mb: 0.5 }}>
|
|
|
|
|
{cleanProductName(productData.name)}
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="body2" color="text.secondary">
|
|
|
|
|
{this.props.t('product.articleNumber')}: {productData.articleNumber}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ textAlign: "right" }}>
|
|
|
|
|
<Typography variant="h6" sx={{ fontWeight: 600, color: "primary.main" }}>
|
|
|
|
|
{new Intl.NumberFormat("de-DE", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "EUR",
|
|
|
|
|
}).format(productData.price)}
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="caption" color="text.secondary">
|
|
|
|
|
inkl. MwSt.
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
@@ -818,23 +1055,8 @@ class ProductDetailPage extends Component {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback to loading message if no prerender content
|
|
|
|
|
return (
|
|
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
p: 4,
|
|
|
|
|
textAlign: "center",
|
|
|
|
|
display: "flex",
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
minHeight: "60vh",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Typography variant="h2" component="h2" gutterBottom>
|
|
|
|
|
Produkt wird geladen...
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
// Fallback to blank page if no prerender content
|
|
|
|
|
return <div style={{ minHeight: "60vh" }} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
@@ -1350,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 <product> 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 <product> 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
|
|
|
|
|
|