feat(ProductDetailPage): implement embedded product rendering and loading
- Add functionality to render embedded products from <product> tags in the product description. - Introduce state management for embedded products and their images, including loading and error handling. - Update localization files to include loading messages for embedded products.
This commit is contained in:
@@ -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;
|
||||
@@ -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 <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
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user