feat: enhance GrowTentKonfigurator with tent filtering and improved rendering
- Added helper functions to filter tent products by shape and generate coverage descriptions based on dimensions. - Implemented logic to handle product image rendering with caching and loading states. - Updated tent selection process to dynamically find and display products based on selected tent shape. - Enhanced user interface with loading indicators and improved layout for product selection.
This commit is contained in:
@@ -10,10 +10,13 @@ import {
|
|||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
|
Grid,
|
||||||
|
CardMedia,
|
||||||
|
CircularProgress,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||||
import { TentShapeSelector, ProductSelector, ExtrasSelector } from '../components/configurator/index.js';
|
import { TentShapeSelector, ProductSelector, ExtrasSelector } from '../components/configurator/index.js';
|
||||||
import { tentShapes, tentSizes, lightTypes, ventilationTypes, extras } from '../data/configuratorData.js';
|
import { tentShapes, lightTypes, ventilationTypes, extras } from '../data/configuratorData.js';
|
||||||
|
|
||||||
function setCachedCategoryData(categoryId, data) {
|
function setCachedCategoryData(categoryId, data) {
|
||||||
if (!window.productCache) {
|
if (!window.productCache) {
|
||||||
@@ -143,12 +146,19 @@ class GrowTentKonfigurator extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`productList:${categoryId}`);
|
|
||||||
window.socketManager.off(`productList:${categoryId}`);
|
window.socketManager.off(`productList:${categoryId}`);
|
||||||
|
|
||||||
|
// Track if we've received the full response to ignore stub response if needed
|
||||||
|
let receivedFullResponse = false;
|
||||||
|
|
||||||
window.socketManager.on(`productList:${categoryId}`,(response) => {
|
window.socketManager.on(`productList:${categoryId}`,(response) => {
|
||||||
console.log("getCategoryProducts full response", response);
|
receivedFullResponse = true;
|
||||||
setCachedCategoryData(categoryId, response);
|
setCachedCategoryData(categoryId, response);
|
||||||
|
|
||||||
|
// Force re-render when data arrives
|
||||||
|
if (categoryId === 'Zelte') {
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||||||
@@ -156,7 +166,13 @@ class GrowTentKonfigurator extends Component {
|
|||||||
"getCategoryProducts",
|
"getCategoryProducts",
|
||||||
{ categoryId: categoryId, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true },
|
{ categoryId: categoryId, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true },
|
||||||
(response) => {
|
(response) => {
|
||||||
console.log("getCategoryProducts stub response", response);
|
// Only process stub response if we haven't received the full response yet
|
||||||
|
if (!receivedFullResponse) {
|
||||||
|
setCachedCategoryData(categoryId, response);
|
||||||
|
if (categoryId === 'Zelte') {
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -188,6 +204,133 @@ class GrowTentKonfigurator extends Component {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Helper function to filter real tent products by shape
|
||||||
|
filterTentsByShape(tentShape, products, attributes) {
|
||||||
|
if (!products || !attributes || !tentShape) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the shape dimensions (e.g., '80x80' -> width: 80, depth: 80)
|
||||||
|
const [widthStr, depthStr] = tentShape.split('x');
|
||||||
|
const targetWidth = parseInt(widthStr);
|
||||||
|
const targetDepth = parseInt(depthStr);
|
||||||
|
|
||||||
|
// Group attributes by product for efficient lookup
|
||||||
|
const productAttributes = {};
|
||||||
|
attributes.forEach(attr => {
|
||||||
|
if (!productAttributes[attr.kArtikel]) {
|
||||||
|
productAttributes[attr.kArtikel] = {};
|
||||||
|
}
|
||||||
|
productAttributes[attr.kArtikel][attr.cName] = attr.cWert;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter products that match the target dimensions
|
||||||
|
const matchingProducts = products.filter(product => {
|
||||||
|
const attrs = productAttributes[product.id];
|
||||||
|
if (!attrs) return false;
|
||||||
|
|
||||||
|
// Check width (Breite)
|
||||||
|
const widthAttr = attrs['Breite'];
|
||||||
|
if (!widthAttr) return false;
|
||||||
|
const widthMatch = widthAttr.match(/(\d+)cm breit/i);
|
||||||
|
if (!widthMatch) return false;
|
||||||
|
const productWidth = parseInt(widthMatch[1]);
|
||||||
|
|
||||||
|
// Check depth (Tiefe)
|
||||||
|
const depthAttr = attrs['Tiefe'];
|
||||||
|
if (!depthAttr) return false;
|
||||||
|
const depthMatch = depthAttr.match(/(\d+)cm tief/i);
|
||||||
|
if (!depthMatch) return false;
|
||||||
|
const productDepth = parseInt(depthMatch[1]);
|
||||||
|
|
||||||
|
// Check if dimensions match
|
||||||
|
return productWidth === targetWidth && productDepth === targetDepth;
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchingProducts.map(product => {
|
||||||
|
// Convert to the format expected by the configurator
|
||||||
|
console.log('Raw product from backend:', product);
|
||||||
|
return product;
|
||||||
|
}); // No sorting needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to generate coverage descriptions
|
||||||
|
getCoverageDescription(width, depth) {
|
||||||
|
const area = width * depth;
|
||||||
|
if (area <= 3600) return '1-2 Pflanzen'; // 60x60
|
||||||
|
if (area <= 6400) return '2-4 Pflanzen'; // 80x80
|
||||||
|
if (area <= 10000) return '4-6 Pflanzen'; // 100x100
|
||||||
|
return '3-6 Pflanzen'; // 120x60 and larger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render tent image using working code from Product component
|
||||||
|
renderTentImage(product) {
|
||||||
|
if (!window.smallPicCache) {
|
||||||
|
window.smallPicCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pictureList = product.pictureList;
|
||||||
|
|
||||||
|
if (!pictureList || pictureList.length === 0 || !pictureList.split(',').length) {
|
||||||
|
return (
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
height={window.innerWidth < 600 ? "240" : "180"}
|
||||||
|
image="/assets/images/nopicture.jpg"
|
||||||
|
alt={product.name || 'Produktbild'}
|
||||||
|
sx={{
|
||||||
|
objectFit: 'contain',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bildId = pictureList.split(',')[0];
|
||||||
|
|
||||||
|
if (window.smallPicCache[bildId]) {
|
||||||
|
return (
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
height={window.innerWidth < 600 ? "240" : "180"}
|
||||||
|
image={window.smallPicCache[bildId]}
|
||||||
|
alt={product.name || 'Produktbild'}
|
||||||
|
sx={{
|
||||||
|
objectFit: 'contain',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load image if not cached
|
||||||
|
if (!this.loadingImages) this.loadingImages = new Set();
|
||||||
|
if (!this.loadingImages.has(bildId)) {
|
||||||
|
this.loadingImages.add(bildId);
|
||||||
|
window.socketManager.emit('getPic', { bildId, size:'small' }, (res) => {
|
||||||
|
if (res.success) {
|
||||||
|
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
this.loadingImages.delete(bildId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CircularProgress sx={{ color: '#90ffc0' }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get real tent products for the selected shape
|
||||||
|
getTentsForShape(shapeId) {
|
||||||
|
const cachedData = getCachedCategoryData('Zelte');
|
||||||
|
if (!cachedData || !cachedData.products || !cachedData.attributes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.filterTentsByShape(shapeId, cachedData.products, cachedData.attributes);
|
||||||
|
}
|
||||||
|
|
||||||
calculateTotalPrice() {
|
calculateTotalPrice() {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
const { selectedTentSize, selectedLightType, selectedVentilationType, selectedExtras } = this.state;
|
const { selectedTentSize, selectedLightType, selectedVentilationType, selectedExtras } = this.state;
|
||||||
@@ -195,9 +338,17 @@ class GrowTentKonfigurator extends Component {
|
|||||||
|
|
||||||
// Add tent price
|
// Add tent price
|
||||||
if (selectedTentSize) {
|
if (selectedTentSize) {
|
||||||
const tent = tentSizes.find(t => t.id === selectedTentSize);
|
// Find the selected tent from all available shapes
|
||||||
if (tent) {
|
let selectedTent = null;
|
||||||
total += tent.price;
|
const allShapes = ['60x60', '80x80', '100x100', '120x60'];
|
||||||
|
for (const shape of allShapes) {
|
||||||
|
const tents = this.getTentsForShape(shape);
|
||||||
|
selectedTent = tents.find(t => t.id === selectedTentSize);
|
||||||
|
if (selectedTent) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTent) {
|
||||||
|
total += selectedTent.price;
|
||||||
itemCount++;
|
itemCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,9 +411,17 @@ class GrowTentKonfigurator extends Component {
|
|||||||
|
|
||||||
// Calculate original total without discount
|
// Calculate original total without discount
|
||||||
if (selectedTentSize) {
|
if (selectedTentSize) {
|
||||||
const tent = tentSizes.find(t => t.id === selectedTentSize);
|
// Find the selected tent from all available shapes
|
||||||
if (tent) {
|
let selectedTent = null;
|
||||||
originalTotal += tent.price;
|
const allShapes = ['60x60', '80x80', '100x100', '120x60'];
|
||||||
|
for (const shape of allShapes) {
|
||||||
|
const tents = this.getTentsForShape(shape);
|
||||||
|
selectedTent = tents.find(t => t.id === selectedTentSize);
|
||||||
|
if (selectedTent) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTent) {
|
||||||
|
originalTotal += selectedTent.price;
|
||||||
itemCount++;
|
itemCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,25 +480,143 @@ class GrowTentKonfigurator extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTentSizeSection() {
|
renderTentSizeSection() {
|
||||||
const { selectedTentSize, selectedTentShape } = this.state;
|
const { selectedTentShape } = this.state;
|
||||||
|
|
||||||
// Filter tents by selected shape
|
|
||||||
const filteredTents = tentSizes.filter(tent => tent.shapeId === selectedTentShape);
|
|
||||||
|
|
||||||
if (!selectedTentShape) {
|
if (!selectedTentShape) {
|
||||||
return null; // Don't show tent sizes until shape is selected
|
return null; // Don't show tent sizes until shape is selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get real filtered tent products for the selected shape
|
||||||
|
const filteredTents = this.getTentsForShape(selectedTentShape);
|
||||||
|
|
||||||
|
// Show loading state if data is not yet available
|
||||||
|
if (filteredTents.length === 0) {
|
||||||
|
const cachedData = getCachedCategoryData('Zelte');
|
||||||
|
if (!cachedData) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
Lade Growbox-Produkte...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// If we have cached data but no filtered tents, show empty state
|
||||||
|
return (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
Keine Produkte für diese Größe verfügbar
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Product display:', {
|
||||||
|
productsCount: filteredTents.length,
|
||||||
|
firstProduct: filteredTents[0] || null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filteredTents.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
Keine Produkte verfügbar
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductSelector
|
<Box sx={{ mb: 4 }}>
|
||||||
products={filteredTents}
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||||
selectedValue={selectedTentSize}
|
2. Growbox Produkt auswählen
|
||||||
onSelect={this.handleTentSizeSelect}
|
</Typography>
|
||||||
productType="tent"
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
title="2. Growbox Produkt auswählen"
|
Wähle das passende Produkt für deine {selectedTentShape} Growbox
|
||||||
subtitle={`Wähle das passende Produkt für deine ${selectedTentShape} Growbox`}
|
</Typography>
|
||||||
gridSize={{ xs: 12, sm: 6, md: 3 }}
|
<Grid container spacing={2}>
|
||||||
/>
|
{filteredTents.map((product, _index) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={product.id}>
|
||||||
|
<Box sx={{
|
||||||
|
width: { xs: '100%', sm: '250px' },
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0px 2px 8px rgba(0,0,0,0.1)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: this.state.selectedTentSize === product.id ? '#2e7d32' : '#e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: '#90caf9'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => this.handleTentSizeSelect(product.id)}>
|
||||||
|
{/* Image */}
|
||||||
|
<Box sx={{
|
||||||
|
height: { xs: '240px', sm: '180px' },
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#ffffff'
|
||||||
|
}}>
|
||||||
|
{this.renderTentImage(product)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{/* Name */}
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
mb: 1,
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{product.name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
color: '#2e7d32',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
mt: 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{product.price ? new Intl.NumberFormat('de-DE', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR'
|
||||||
|
}).format(product.price) : 'Kein Preis'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* VAT */}
|
||||||
|
<Typography variant="body2" sx={{ color: '#77aa77', fontSize: '0.75em' }}>
|
||||||
|
(incl. 19% MwSt.,*)
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Selection Indicator */}
|
||||||
|
{this.state.selectedTentSize === product.id && (
|
||||||
|
<Box sx={{ mt: 1, textAlign: 'center' }}>
|
||||||
|
<Typography variant="body2" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||||
|
✓ Ausgewählt
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +668,14 @@ class GrowTentKonfigurator extends Component {
|
|||||||
renderInlineSummary() {
|
renderInlineSummary() {
|
||||||
const { selectedTentSize, selectedLightType, selectedVentilationType, selectedExtras, totalPrice } = this.state;
|
const { selectedTentSize, selectedLightType, selectedVentilationType, selectedExtras, totalPrice } = this.state;
|
||||||
|
|
||||||
const selectedTent = tentSizes.find(t => t.id === selectedTentSize);
|
// Find the selected tent from all available shapes
|
||||||
|
let selectedTent = null;
|
||||||
|
const allShapes = ['60x60', '80x80', '100x100', '120x60'];
|
||||||
|
for (const shape of allShapes) {
|
||||||
|
const tents = this.getTentsForShape(shape);
|
||||||
|
selectedTent = tents.find(t => t.id === selectedTentSize);
|
||||||
|
if (selectedTent) break;
|
||||||
|
}
|
||||||
const selectedLight = lightTypes.find(l => l.id === selectedLightType);
|
const selectedLight = lightTypes.find(l => l.id === selectedLightType);
|
||||||
const selectedVentilation = ventilationTypes.find(v => v.id === selectedVentilationType);
|
const selectedVentilation = ventilationTypes.find(v => v.id === selectedVentilationType);
|
||||||
const selectedExtrasItems = extras.filter(e => selectedExtras.includes(e.id));
|
const selectedExtrasItems = extras.filter(e => selectedExtras.includes(e.id));
|
||||||
|
|||||||
Reference in New Issue
Block a user