Compare commits

...

2 Commits

Author SHA1 Message Date
sebseb7
b267b9132a 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.
2025-11-17 10:16:16 +01:00
sebseb7
c82a6a8f62 u 2025-11-17 09:19:57 +01:00
3 changed files with 260 additions and 22 deletions

View File

@@ -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

View File

@@ -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:",

View File

@@ -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: