1505 lines
57 KiB
JavaScript
1505 lines
57 KiB
JavaScript
import React, { Component } from 'react';
|
|
import {
|
|
Container,
|
|
Paper,
|
|
Box,
|
|
Typography,
|
|
Button,
|
|
Divider,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
ListItemSecondaryAction,
|
|
Grid,
|
|
CardMedia,
|
|
CircularProgress,
|
|
Stack,
|
|
} from '@mui/material';
|
|
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
|
import { TentShapeSelector, ExtrasSelector } from '../components/configurator/index.js';
|
|
import { tentShapes } from '../data/configuratorData.js';
|
|
import { Link } from 'react-router-dom';
|
|
import IconButton from '@mui/material/IconButton';
|
|
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
|
|
|
function setCachedCategoryData(categoryId, data, language = 'de') {
|
|
if (!window.productCache) {
|
|
window.productCache = {};
|
|
}
|
|
if (!window.productDetailCache) {
|
|
window.productDetailCache = {};
|
|
}
|
|
|
|
try {
|
|
const cacheKey = `categoryProducts_${categoryId}_${language}`;
|
|
if(data.products) for(const product of data.products) {
|
|
const productCacheKey = `product_${product.id}_${language}`;
|
|
window.productDetailCache[productCacheKey] = product;
|
|
}
|
|
window.productCache[cacheKey] = {
|
|
...data,
|
|
timestamp: Date.now()
|
|
};
|
|
} catch (err) {
|
|
console.error('Error writing to cache:', err);
|
|
}
|
|
}
|
|
function getCachedCategoryData(categoryId, language = 'de') {
|
|
if (!window.productCache) {
|
|
window.productCache = {};
|
|
}
|
|
|
|
try {
|
|
const cacheKey = `categoryProducts_${categoryId}_${language}`;
|
|
const cachedData = window.productCache[cacheKey];
|
|
|
|
if (cachedData) {
|
|
const { timestamp } = cachedData;
|
|
const cacheAge = Date.now() - timestamp;
|
|
const tenMinutes = 10 * 60 * 1000;
|
|
if (cacheAge < tenMinutes) {
|
|
return cachedData;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Error reading from cache:', err);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
import { withI18n } from '../i18n/withTranslation.js';
|
|
|
|
class GrowTentKonfigurator extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
// Try to restore state from window object
|
|
const savedState = window.growTentKonfiguratorState;
|
|
|
|
this.state = {
|
|
selectedTentShape: savedState?.selectedTentShape || '80x80',
|
|
selectedTentSize: savedState?.selectedTentSize || 'tent_80x80x160',
|
|
selectedLightType: savedState?.selectedLightType || 'led_quantum_board',
|
|
selectedVentilationType: savedState?.selectedVentilationType || 'premium_ventilation',
|
|
selectedExtras: savedState?.selectedExtras || [],
|
|
totalPrice: savedState?.totalPrice || 0,
|
|
categoryLoadStatus: {
|
|
"Zelte": false,
|
|
"Lampen": false,
|
|
"Abluft-sets": false,
|
|
"Set-zubehoer": false
|
|
}
|
|
};
|
|
|
|
this.handleTentShapeSelect = this.handleTentShapeSelect.bind(this);
|
|
this.handleTentSizeSelect = this.handleTentSizeSelect.bind(this);
|
|
this.handleLightTypeSelect = this.handleLightTypeSelect.bind(this);
|
|
this.handleVentilationSelect = this.handleVentilationSelect.bind(this);
|
|
this.handleExtraToggle = this.handleExtraToggle.bind(this);
|
|
this.calculateTotalPrice = this.calculateTotalPrice.bind(this);
|
|
this.saveStateToWindow = this.saveStateToWindow.bind(this);
|
|
this.checkCacheValidity = this.checkCacheValidity.bind(this);
|
|
this.handleAddToCart = this.handleAddToCart.bind(this);
|
|
}
|
|
|
|
saveStateToWindow() {
|
|
// Save current state to window object for backup
|
|
window.growTentKonfiguratorState = {
|
|
selectedTentShape: this.state.selectedTentShape,
|
|
selectedTentSize: this.state.selectedTentSize,
|
|
selectedLightType: this.state.selectedLightType,
|
|
selectedVentilationType: this.state.selectedVentilationType,
|
|
selectedExtras: this.state.selectedExtras,
|
|
totalPrice: this.state.totalPrice,
|
|
timestamp: Date.now() // Add timestamp for debugging
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
// @note Calculate initial total price with preselected products
|
|
//this.calculateTotalPrice();
|
|
this.fetchCategoryData("Zelte");
|
|
this.fetchCategoryData("Lampen");
|
|
this.fetchCategoryData("Abluft-sets");
|
|
this.fetchCategoryData("Set-zubehoer");
|
|
this.cacheCheckInterval = setInterval(this.checkCacheValidity, 60000);
|
|
}
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
// Reset tent size selection if shape changes
|
|
if (prevState.selectedTentShape !== this.state.selectedTentShape && this.state.selectedTentShape !== prevState.selectedTentShape) {
|
|
this.setState({ selectedTentSize: '' });
|
|
}
|
|
|
|
// Reset ventilation selection if current selection is not deliverable for new tent shape
|
|
if (prevState.selectedTentShape !== this.state.selectedTentShape && this.state.selectedVentilationType) {
|
|
const availableVentilation = this.getVentilationForTentShape(this.state.selectedTentShape);
|
|
const currentVentilation = availableVentilation.find(v => v.id === this.state.selectedVentilationType);
|
|
if (!currentVentilation || !currentVentilation.isDeliverable) {
|
|
this.setState({ selectedVentilationType: '' });
|
|
}
|
|
}
|
|
|
|
// Recalculate total price when selections change
|
|
if (
|
|
prevState.selectedTentSize !== this.state.selectedTentSize ||
|
|
prevState.selectedLightType !== this.state.selectedLightType ||
|
|
prevState.selectedVentilationType !== this.state.selectedVentilationType ||
|
|
prevState.selectedExtras !== this.state.selectedExtras
|
|
) {
|
|
this.calculateTotalPrice();
|
|
}
|
|
|
|
// Save state to window object whenever selections change
|
|
if (
|
|
prevState.selectedTentShape !== this.state.selectedTentShape ||
|
|
prevState.selectedTentSize !== this.state.selectedTentSize ||
|
|
prevState.selectedLightType !== this.state.selectedLightType ||
|
|
prevState.selectedVentilationType !== this.state.selectedVentilationType ||
|
|
prevState.selectedExtras !== this.state.selectedExtras ||
|
|
prevState.totalPrice !== this.state.totalPrice
|
|
) {
|
|
this.saveStateToWindow();
|
|
}
|
|
|
|
// Check if language changed and reload category data
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language;
|
|
const prevLanguage = prevProps.languageContext?.currentLanguage || prevProps.i18n?.language;
|
|
|
|
if (currentLanguage && prevLanguage && currentLanguage !== prevLanguage) {
|
|
console.log(`Language changed from ${prevLanguage} to ${currentLanguage} - reloading configurator data`);
|
|
|
|
// Reset category load status to trigger loading state UI
|
|
this.setState({
|
|
categoryLoadStatus: {
|
|
"Zelte": false,
|
|
"Lampen": false,
|
|
"Abluft-sets": false,
|
|
"Set-zubehoer": false
|
|
}
|
|
}, () => {
|
|
// Fetch data for all categories with new language
|
|
this.fetchCategoryData("Zelte");
|
|
this.fetchCategoryData("Lampen");
|
|
this.fetchCategoryData("Abluft-sets");
|
|
this.fetchCategoryData("Set-zubehoer");
|
|
});
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.cacheCheckInterval) {
|
|
clearInterval(this.cacheCheckInterval);
|
|
}
|
|
}
|
|
fetchCategoryData(categoryId) {
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
|
const cachedData = getCachedCategoryData(categoryId, currentLanguage);
|
|
if (cachedData) {
|
|
this.setState(prevState => ({
|
|
categoryLoadStatus: {
|
|
...prevState.categoryLoadStatus,
|
|
[categoryId]: true
|
|
}
|
|
}));
|
|
return;
|
|
}
|
|
|
|
console.log(`Cache miss for category ${categoryId}, fetching...`);
|
|
|
|
window.socketManager.off(`productList:${categoryId}`);
|
|
|
|
window.socketManager.on(`productList:${categoryId}`,(response) => {
|
|
setCachedCategoryData(categoryId, response, currentLanguage);
|
|
this.setState(prevState => ({
|
|
categoryLoadStatus: {
|
|
...prevState.categoryLoadStatus,
|
|
[categoryId]: true
|
|
}
|
|
}));
|
|
});
|
|
window.socketManager.emit(
|
|
"getCategoryProducts",
|
|
{ categoryId: categoryId, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true },
|
|
() => {}
|
|
);
|
|
}
|
|
|
|
checkCacheValidity() {
|
|
const categories = ["Zelte", "Lampen", "Abluft-sets", "Set-zubehoer"];
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
|
let needsUpdate = false;
|
|
const newStatus = { ...this.state.categoryLoadStatus };
|
|
for (const categoryId of categories) {
|
|
if (newStatus[categoryId] && !getCachedCategoryData(categoryId, currentLanguage)) {
|
|
console.log(`Refetching ${categoryId} due to cache miss/expiry`);
|
|
newStatus[categoryId] = false;
|
|
needsUpdate = true;
|
|
this.fetchCategoryData(categoryId);
|
|
}
|
|
}
|
|
if (needsUpdate) {
|
|
this.setState({ categoryLoadStatus: newStatus });
|
|
}
|
|
}
|
|
|
|
handleTentShapeSelect(shapeId) {
|
|
this.setState({ selectedTentShape: shapeId });
|
|
}
|
|
|
|
handleTentSizeSelect(tentId) {
|
|
this.setState({ selectedTentSize: tentId });
|
|
}
|
|
|
|
handleLightTypeSelect(lightId) {
|
|
this.setState({ selectedLightType: lightId });
|
|
}
|
|
|
|
handleVentilationSelect(ventilationId) {
|
|
this.setState({ selectedVentilationType: ventilationId });
|
|
}
|
|
|
|
handleExtraToggle(extraId) {
|
|
const { selectedExtras } = this.state;
|
|
const newSelectedExtras = selectedExtras.includes(extraId)
|
|
? selectedExtras.filter(id => id !== extraId)
|
|
: [...selectedExtras, extraId];
|
|
|
|
this.setState({ selectedExtras: newSelectedExtras });
|
|
}
|
|
|
|
handleAddToCart() {
|
|
if (!window.cart) window.cart = [];
|
|
|
|
// Collect components
|
|
const setComponents = [];
|
|
|
|
// Tent
|
|
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 === this.state.selectedTentSize);
|
|
if (selectedTent) {
|
|
setComponents.push(selectedTent);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Light
|
|
if (this.state.selectedLightType) {
|
|
const availableLamps = this.getLampsForTentShape(this.state.selectedTentShape);
|
|
const light = availableLamps.find(l => l.id === this.state.selectedLightType);
|
|
if (light) setComponents.push(light);
|
|
}
|
|
|
|
// Ventilation
|
|
if (this.state.selectedVentilationType) {
|
|
const availableVentilation = this.getVentilationForTentShape(this.state.selectedTentShape);
|
|
const ventilation = availableVentilation.find(v => v.id === this.state.selectedVentilationType);
|
|
if (ventilation) setComponents.push(ventilation);
|
|
}
|
|
|
|
// Extras
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
|
const extrasData = getCachedCategoryData('Set-zubehoer', currentLanguage);
|
|
if (extrasData && Array.isArray(extrasData.products)) {
|
|
this.state.selectedExtras.forEach(extraId => {
|
|
const extra = extrasData.products.find(e => e.id === extraId);
|
|
if (extra) setComponents.push(extra);
|
|
});
|
|
}
|
|
|
|
// Now push to cart
|
|
const totalPrice = this.state.totalPrice;
|
|
const setName = this.props.t ? this.props.t("kitConfig.setName", { shape: this.state.selectedTentShape }) : `Growbox Set - ${this.state.selectedTentShape}`;
|
|
|
|
window.cart.push({
|
|
id: null,
|
|
name: setName,
|
|
seoName: null,
|
|
pictureList: null,
|
|
price: totalPrice,
|
|
fGrundPreis: null,
|
|
cGrundEinheit: null,
|
|
quantity: 1,
|
|
weight: null,
|
|
vat: 19,
|
|
versandklasse: null,
|
|
availableSupplier: 0,
|
|
komponenten: setComponents,
|
|
available: 1
|
|
});
|
|
|
|
window.dispatchEvent(new CustomEvent("cart"));
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
const sizeMatch = productWidth === targetWidth && productDepth === targetDepth;
|
|
|
|
// Check availability - only show products that are available for delivery
|
|
// Following same logic as Content.js: available > 0 OR availableSupplier == 1
|
|
const isAvailable = (product.available > 0) || (product.availableSupplier === 1);
|
|
|
|
return sizeMatch && isAvailable;
|
|
});
|
|
|
|
return matchingProducts.map(product => {
|
|
return product;
|
|
}); // No sorting needed
|
|
}
|
|
|
|
// Filter lamps by tent size using "passend für Zelt" attribute
|
|
filterLampsByTentSize(tentShape, products, attributes) {
|
|
if (!products || !attributes || !tentShape) return [];
|
|
|
|
// Group attributes by product ID and attribute name
|
|
const attributesByProduct = {};
|
|
attributes.forEach(attr => {
|
|
if (!attributesByProduct[attr.kArtikel]) {
|
|
attributesByProduct[attr.kArtikel] = {};
|
|
}
|
|
attributesByProduct[attr.kArtikel][attr.cName] = attr.cWert;
|
|
});
|
|
|
|
// Filter products by matching tent size in "passend für Zelt" attribute
|
|
const matchingProducts = products.filter(product => {
|
|
const attrs = attributesByProduct[product.id];
|
|
if (!attrs) return false;
|
|
|
|
// Check "passend für Zelt" attribute
|
|
const tentSizeAttr = attrs['passend für Zelt'];
|
|
if (!tentSizeAttr) return false;
|
|
|
|
// Check if tent size matches
|
|
const sizeMatch = tentSizeAttr === tentShape;
|
|
|
|
// Check availability - only show products that are available for delivery
|
|
const isAvailable = (product.available > 0) || (product.availableSupplier === 1);
|
|
return sizeMatch && isAvailable;
|
|
});
|
|
|
|
return matchingProducts;
|
|
}
|
|
|
|
// Get lamps for selected tent shape
|
|
getLampsForTentShape(shapeId) {
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
|
const cachedData = getCachedCategoryData('Lampen', currentLanguage);
|
|
if (!cachedData || !cachedData.products || !cachedData.attributes) {
|
|
return [];
|
|
}
|
|
|
|
return this.filterLampsByTentSize(shapeId, cachedData.products, cachedData.attributes);
|
|
}
|
|
|
|
// Get ventilation products for selected tent shape
|
|
getVentilationForTentShape(shapeId) {
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
|
const cachedData = getCachedCategoryData('Abluft-sets', currentLanguage);
|
|
if (!cachedData || !cachedData.products || !cachedData.attributes) {
|
|
return [];
|
|
}
|
|
|
|
return this.filterVentilationByTentSize(shapeId, cachedData.products, cachedData.attributes);
|
|
}
|
|
|
|
// Filter ventilation by tent size using "passend für Zelt" attribute
|
|
filterVentilationByTentSize(tentShape, products, attributes) {
|
|
if (!products || !attributes || !tentShape) return [];
|
|
|
|
// Group attributes by product ID and attribute name
|
|
const attributesByProduct = {};
|
|
attributes.forEach(attr => {
|
|
if (!attributesByProduct[attr.kArtikel]) {
|
|
attributesByProduct[attr.kArtikel] = {};
|
|
}
|
|
attributesByProduct[attr.kArtikel][attr.cName] = attr.cWert;
|
|
});
|
|
|
|
// Filter products by matching tent size in "passend für Zelt" attribute
|
|
// Include all size-matching products, but mark availability status
|
|
const matchingProducts = products.filter(product => {
|
|
const attrs = attributesByProduct[product.id];
|
|
if (!attrs) return false;
|
|
|
|
// Check "passend für Zelt" attribute
|
|
const tentSizeAttr = attrs['passend für Zelt'];
|
|
if (!tentSizeAttr) return false;
|
|
|
|
// Only filter by tent size match, not by availability
|
|
const sizeMatch = tentSizeAttr === tentShape;
|
|
return sizeMatch;
|
|
}).map(product => {
|
|
// Add availability flag to each product for UI rendering
|
|
const isAvailable = (product.available > 0) || (product.availableSupplier === 1);
|
|
return {
|
|
...product,
|
|
isDeliverable: isAvailable
|
|
};
|
|
});
|
|
|
|
return matchingProducts;
|
|
}
|
|
|
|
// Helper function to generate coverage descriptions
|
|
getCoverageDescription(width, depth) {
|
|
const { t } = this.props;
|
|
const area = width * depth;
|
|
if (area <= 3600) return t ? t('kitConfig.plants1to2') : '1-2 Pflanzen'; // 60x60
|
|
if (area <= 6400) return t ? t('kitConfig.plants2to4') : '2-4 Pflanzen'; // 80x80
|
|
if (area <= 10000) return t ? t('kitConfig.plants4to6') : '4-6 Pflanzen'; // 100x100
|
|
return t ? t('kitConfig.plants3to6') : '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/avif' }));
|
|
this.forceUpdate();
|
|
}
|
|
this.loadingImages.delete(bildId);
|
|
});
|
|
}
|
|
|
|
return (
|
|
<CircularProgress sx={{ color: '#90ffc0' }} />
|
|
);
|
|
}
|
|
|
|
// Get real tent products for the selected shape
|
|
getTentsForShape(shapeId) {
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
|
const cachedData = getCachedCategoryData('Zelte', currentLanguage);
|
|
if (!cachedData || !cachedData.products || !cachedData.attributes) {
|
|
return [];
|
|
}
|
|
|
|
return this.filterTentsByShape(shapeId, cachedData.products, cachedData.attributes);
|
|
}
|
|
|
|
calculateTotalPrice() {
|
|
let total = 0;
|
|
const { selectedTentSize, selectedLightType, selectedVentilationType, selectedExtras } = this.state;
|
|
let itemCount = 0;
|
|
|
|
// Add tent price
|
|
if (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;
|
|
}
|
|
|
|
if (selectedTent) {
|
|
total += selectedTent.price;
|
|
itemCount++;
|
|
}
|
|
}
|
|
|
|
// Add light price
|
|
if (selectedLightType) {
|
|
const availableLamps = this.getLampsForTentShape(this.state.selectedTentShape);
|
|
const light = availableLamps.find(l => l.id === selectedLightType);
|
|
if (light && light.price) {
|
|
total += light.price;
|
|
itemCount++;
|
|
}
|
|
}
|
|
|
|
// Add ventilation price
|
|
if (selectedVentilationType) {
|
|
const availableVentilation = this.getVentilationForTentShape(this.state.selectedTentShape);
|
|
const ventilation = availableVentilation.find(v => v.id === selectedVentilationType);
|
|
if (ventilation && ventilation.price && ventilation.isDeliverable) {
|
|
total += ventilation.price;
|
|
itemCount++;
|
|
}
|
|
}
|
|
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
|
const extrasData = getCachedCategoryData('Set-zubehoer', currentLanguage);
|
|
if (!extrasData || !Array.isArray(extrasData.products)) {
|
|
console.warn('Extras data not available; skipping extras price in total calculation');
|
|
} else {
|
|
// Add extras prices
|
|
selectedExtras.forEach(extraId => {
|
|
const extra = extrasData.products.find(e => e.id === extraId);
|
|
if (extra) {
|
|
total += extra.price;
|
|
itemCount++;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Apply bundle discount
|
|
let discountPercentage = 0;
|
|
if (itemCount >= 3) discountPercentage = 15; // 15% for 3+ items
|
|
if (itemCount >= 5) discountPercentage = 22; // 22% for 5+ items
|
|
if (itemCount >= 7) discountPercentage = 28; // 28% for 7+ items
|
|
|
|
const discountedTotal = total * (1 - discountPercentage / 100);
|
|
this.setState({ totalPrice: discountedTotal });
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
formatPrice(price) {
|
|
return new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(price);
|
|
}
|
|
|
|
calculateSavings() {
|
|
// Bundle discount calculation
|
|
const { selectedTentSize, selectedLightType, selectedVentilationType, selectedExtras } = this.state;
|
|
let itemCount = 0;
|
|
let originalTotal = 0;
|
|
|
|
// Calculate original total without discount
|
|
if (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;
|
|
}
|
|
|
|
if (selectedTent) {
|
|
originalTotal += selectedTent.price;
|
|
itemCount++;
|
|
}
|
|
}
|
|
|
|
if (selectedLightType) {
|
|
const availableLamps = this.getLampsForTentShape(this.state.selectedTentShape);
|
|
const light = availableLamps.find(l => l.id === selectedLightType);
|
|
if (light && light.price) {
|
|
originalTotal += light.price;
|
|
itemCount++;
|
|
}
|
|
}
|
|
|
|
if (selectedVentilationType) {
|
|
const availableVentilation = this.getVentilationForTentShape(this.state.selectedTentShape);
|
|
const ventilation = availableVentilation.find(v => v.id === selectedVentilationType);
|
|
if (ventilation && ventilation.price && ventilation.isDeliverable) {
|
|
originalTotal += ventilation.price;
|
|
itemCount++;
|
|
}
|
|
}
|
|
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
|
const extrasData = getCachedCategoryData('Set-zubehoer', currentLanguage);
|
|
if (!extrasData || !Array.isArray(extrasData.products)) {
|
|
console.warn('Extras data not available; skipping extras in savings calculation');
|
|
} else {
|
|
selectedExtras.forEach(extraId => {
|
|
const extra = extrasData.products.find(e => e.id === extraId);
|
|
if (extra) {
|
|
originalTotal += extra.price;
|
|
itemCount++;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Progressive discount based on number of selected items
|
|
let discountPercentage = 0;
|
|
if (itemCount >= 3) discountPercentage = 15; // 15% for 3+ items
|
|
if (itemCount >= 5) discountPercentage = 22; // 22% for 5+ items
|
|
if (itemCount >= 7) discountPercentage = 28; // 28% for 7+ items
|
|
|
|
const savings = originalTotal * (discountPercentage / 100);
|
|
|
|
return {
|
|
savings: savings,
|
|
discountPercentage: discountPercentage,
|
|
hasDiscount: discountPercentage > 0
|
|
};
|
|
}
|
|
|
|
renderTentShapeSection() {
|
|
const { selectedTentShape } = this.state;
|
|
const { t } = this.props;
|
|
|
|
return (
|
|
<TentShapeSelector
|
|
tentShapes={tentShapes}
|
|
selectedShape={selectedTentShape}
|
|
onShapeSelect={this.handleTentShapeSelect}
|
|
title={t ? t("kitConfig.selectShapeTitle") : "1. Growbox-Form auswählen"}
|
|
subtitle={t ? t("kitConfig.selectShapeSubtitle") : "Wähle zuerst die Grundfläche deiner Growbox aus"}
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderTentSizeSection() {
|
|
const { selectedTentShape } = this.state;
|
|
const { t } = this.props;
|
|
|
|
if (!selectedTentShape) {
|
|
return null; // Don't show tent sizes until shape is selected
|
|
}
|
|
|
|
if (!this.state.categoryLoadStatus["Zelte"]) {
|
|
return (
|
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
|
<Typography variant="h6" color="text.secondary">
|
|
{t ? t("kitConfig.loadingProducts") : "Lade Growbox-Produkte..."}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Get real filtered tent products for the selected shape
|
|
const filteredTents = this.getTentsForShape(selectedTentShape);
|
|
|
|
if (filteredTents.length === 0) {
|
|
return (
|
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
|
<Typography variant="h6" color="text.secondary">
|
|
{t ? t("kitConfig.noProductsAvailable") : "Keine Produkte für diese Größe verfügbar"}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
return (
|
|
<Box sx={{ mb: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectProductTitle") : "2. Growbox Produkt auswählen"}
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
|
{t ? t("kitConfig.selectProductSubtitle", { shape: selectedTentShape }) : `Wähle das passende Produkt für deine ${selectedTentShape} Growbox`}
|
|
</Typography>
|
|
<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',
|
|
overflow: 'hidden',
|
|
cursor: 'pointer',
|
|
border: '2px solid',
|
|
borderColor: this.state.selectedTentSize === product.id ? '#2e7d32' : '#e0e0e0',
|
|
backgroundColor: this.state.selectedTentSize === product.id ? '#f1f8e9' : '#ffffff',
|
|
'&:hover': {
|
|
boxShadow: 6,
|
|
borderColor: this.state.selectedTentSize === product.id ? '#2e7d32' : '#90caf9'
|
|
},
|
|
transition: 'all 0.3s ease'
|
|
}}
|
|
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" gutterBottom sx={{ fontWeight: 'bold' }}>
|
|
{product.name}
|
|
</Typography>
|
|
<Typography gutterBottom>
|
|
{product.kurzBeschreibung}
|
|
</Typography>
|
|
|
|
{/* Price with VAT - Same as Product.js */}
|
|
<Typography variant="h6" sx={{
|
|
color: '#2e7d32',
|
|
fontWeight: 'bold',
|
|
mt: 2,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
flexWrap: 'wrap'
|
|
}}>
|
|
<span>{product.price ? new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(product.price) : (t ? t("kitConfig.noPrice") : 'Kein Preis')}</span>
|
|
{product.vat && (
|
|
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
|
({t ? t("product.inclVat", { vat: product.vat }) : `incl. ${product.vat}% MwSt.,*`})
|
|
</small>
|
|
)}
|
|
</Typography>
|
|
|
|
{/* Selection Indicator - Separate line */}
|
|
{this.state.selectedTentSize === product.id && (
|
|
<Typography variant="body2" sx={{
|
|
color: '#2e7d32',
|
|
fontWeight: 'bold',
|
|
mt: 1,
|
|
textAlign: 'center'
|
|
}}>
|
|
{t ? t("kitConfig.selected") : "✓ Ausgewählt"}
|
|
</Typography>
|
|
)}
|
|
<Stack direction="row" spacing={1} justifyContent="center">
|
|
<IconButton
|
|
component={Link}
|
|
to={`/Artikel/${product.seoName}`}
|
|
size="small"
|
|
aria-label="Produktdetails anzeigen"
|
|
sx={{ mr: 1, color: 'text.secondary' }}
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<ZoomInIcon />
|
|
</IconButton>
|
|
</Stack>
|
|
</Box>
|
|
</Box>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
renderLightSection() {
|
|
const { selectedLightType, selectedTentShape } = this.state;
|
|
const { t } = this.props;
|
|
|
|
if (!selectedTentShape) {
|
|
return (
|
|
<Box sx={{ mt: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectLightingTitle") : "3. Beleuchtung wählen"}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{t ? t("kitConfig.selectLightingSubtitle") : "Bitte wählen Sie zuerst eine Zeltgröße aus."}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (!this.state.categoryLoadStatus["Lampen"]) {
|
|
return (
|
|
<Box sx={{ mt: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectLightingTitleShape", { shape: selectedTentShape }) : `3. Beleuchtung wählen - ${selectedTentShape}`}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{t ? t("kitConfig.loadingLighting") : "Lade Beleuchtungs-Produkte..."}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const availableLamps = this.getLampsForTentShape(selectedTentShape);
|
|
|
|
if (availableLamps.length === 0) {
|
|
return (
|
|
<Box sx={{ mt: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectLightingTitleShape", { shape: selectedTentShape }) : `3. Beleuchtung wählen - ${selectedTentShape}`}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{t ? t("kitConfig.noLightingAvailable", { shape: selectedTentShape }) : `Keine passenden Lampen für Zeltgröße ${selectedTentShape} verfügbar.`}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ mt: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectLightingTitleShape", { shape: selectedTentShape }) : `3. Beleuchtung wählen - ${selectedTentShape}`}
|
|
</Typography>
|
|
<Grid container spacing={2}>
|
|
{availableLamps.map((lamp) => (
|
|
<Grid item xs={12} sm={6} md={4} key={lamp.id}>
|
|
<Box sx={{
|
|
width: { xs: '100%', sm: '250px' },
|
|
height: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
borderRadius: '8px',
|
|
overflow: 'hidden',
|
|
cursor: 'pointer',
|
|
border: '2px solid',
|
|
borderColor: selectedLightType === lamp.id ? '#2e7d32' : '#e0e0e0',
|
|
backgroundColor: selectedLightType === lamp.id ? '#f1f8e9' : '#ffffff',
|
|
'&:hover': {
|
|
boxShadow: 6,
|
|
borderColor: '#2e7d32'
|
|
},
|
|
transition: 'all 0.3s ease'
|
|
}}
|
|
onClick={() => this.handleLightTypeSelect(lamp.id)}
|
|
>
|
|
{/* Image */}
|
|
<Box sx={{ height: 200, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
|
|
{this.renderTentImage(lamp)}
|
|
</Box>
|
|
|
|
{/* Content */}
|
|
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
|
{/* Name */}
|
|
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>
|
|
{lamp.name}
|
|
</Typography>
|
|
<Typography gutterBottom>
|
|
{lamp.kurzBeschreibung}
|
|
</Typography>
|
|
|
|
{/* Price with VAT */}
|
|
<Typography variant="h6" sx={{
|
|
color: '#2e7d32',
|
|
fontWeight: 'bold',
|
|
mt: 2,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
flexWrap: 'wrap'
|
|
}}>
|
|
<span>{lamp.price ? new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(lamp.price) : (t ? t("kitConfig.noPrice") : 'Kein Preis')}</span>
|
|
{lamp.vat && (
|
|
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
|
({t ? t("product.inclVat", { vat: lamp.vat }) : `incl. ${lamp.vat}% MwSt.,*`})
|
|
</small>
|
|
)}
|
|
</Typography>
|
|
|
|
{/* Selection Indicator */}
|
|
{selectedLightType === lamp.id && (
|
|
<Typography variant="body2" sx={{
|
|
color: '#2e7d32',
|
|
fontWeight: 'bold',
|
|
mt: 1,
|
|
textAlign: 'center'
|
|
}}>
|
|
{t ? t("kitConfig.selected") : "✓ Ausgewählt"}
|
|
</Typography>
|
|
)}
|
|
<Stack direction="row" spacing={1} justifyContent="center">
|
|
<IconButton
|
|
component={Link}
|
|
to={`/Artikel/${lamp.seoName}`}
|
|
size="small"
|
|
aria-label="Produktdetails anzeigen"
|
|
sx={{ mr: 1, color: 'text.secondary' }}
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<ZoomInIcon />
|
|
</IconButton>
|
|
</Stack>
|
|
</Box>
|
|
</Box>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
renderVentilationSection() {
|
|
const { selectedVentilationType, selectedTentShape } = this.state;
|
|
const { t } = this.props;
|
|
|
|
if (!selectedTentShape) {
|
|
return (
|
|
<Box sx={{ mt: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectVentilationTitle") : "4. Belüftung auswählen"}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{t ? t("kitConfig.selectVentilationSubtitle") : "Bitte wählen Sie zuerst eine Zeltgröße aus."}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (!this.state.categoryLoadStatus["Abluft-sets"]) {
|
|
return (
|
|
<Box sx={{ mt: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectVentilationTitleShape", { shape: selectedTentShape }) : `4. Belüftung auswählen - ${selectedTentShape}`}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{t ? t("kitConfig.loadingVentilation") : "Lade Belüftungs-Produkte..."}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const availableVentilation = this.getVentilationForTentShape(selectedTentShape);
|
|
|
|
if (availableVentilation.length === 0) {
|
|
return (
|
|
<Box sx={{ mt: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectVentilationTitleShape", { shape: selectedTentShape }) : `4. Belüftung auswählen - ${selectedTentShape}`}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{t ? t("kitConfig.noVentilationAvailable", { shape: selectedTentShape }) : `Keine passenden Belüftung für Zeltgröße ${selectedTentShape} verfügbar.`}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ mt: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectVentilationTitleShape", { shape: selectedTentShape }) : `4. Belüftung auswählen - ${selectedTentShape}`}
|
|
</Typography>
|
|
<Grid container spacing={2}>
|
|
{availableVentilation.map((ventilation) => (
|
|
<Grid item xs={12} sm={6} md={4} key={ventilation.id}>
|
|
<Box sx={{
|
|
width: { xs: '100%', sm: '250px' },
|
|
height: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
borderRadius: '8px',
|
|
overflow: 'hidden',
|
|
cursor: ventilation.isDeliverable ? 'pointer' : 'not-allowed',
|
|
border: '2px solid',
|
|
borderColor: selectedVentilationType === ventilation.id ? '#2e7d32' : '#e0e0e0',
|
|
backgroundColor: selectedVentilationType === ventilation.id ? '#f1f8e9' : '#ffffff',
|
|
opacity: ventilation.isDeliverable ? 1 : 0.5,
|
|
filter: ventilation.isDeliverable ? 'none' : 'grayscale(50%)',
|
|
'&:hover': ventilation.isDeliverable ? {
|
|
boxShadow: 6,
|
|
borderColor: '#2e7d32'
|
|
} : {},
|
|
transition: 'all 0.3s ease',
|
|
position: 'relative'
|
|
}}
|
|
onClick={() => {
|
|
if (ventilation.isDeliverable) {
|
|
this.handleVentilationSelect(ventilation.id);
|
|
}
|
|
}}
|
|
>
|
|
{/* Non-deliverable overlay */}
|
|
{!ventilation.isDeliverable && (
|
|
<Box sx={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
|
zIndex: 1,
|
|
flexDirection: 'column'
|
|
}}>
|
|
<Typography variant="h6" sx={{
|
|
color: '#d32f2f',
|
|
fontWeight: 'bold',
|
|
textAlign: 'center',
|
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
p: 1,
|
|
borderRadius: 1
|
|
}}>
|
|
{t ? t("kitConfig.notDeliverable") : "Nicht lieferbar"}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Image */}
|
|
<Box sx={{ height: 200, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
|
|
{this.renderTentImage(ventilation)}
|
|
</Box>
|
|
|
|
{/* Content */}
|
|
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
|
{/* Name */}
|
|
<Typography variant="h6" gutterBottom sx={{
|
|
fontWeight: 'bold',
|
|
color: ventilation.isDeliverable ? 'inherit' : '#999'
|
|
}}>
|
|
{ventilation.name}
|
|
</Typography>
|
|
<Typography gutterBottom>
|
|
{ventilation.kurzBeschreibung}
|
|
</Typography>
|
|
|
|
{/* Price with VAT */}
|
|
<Typography variant="h6" sx={{
|
|
color: ventilation.isDeliverable ? '#2e7d32' : '#999',
|
|
fontWeight: 'bold',
|
|
mt: 2,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
flexWrap: 'wrap'
|
|
}}>
|
|
<span>{ventilation.price ? new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(ventilation.price) : (t ? t("kitConfig.noPrice") : 'Kein Preis')}</span>
|
|
{ventilation.vat && (
|
|
<small style={{ color: ventilation.isDeliverable ? '#77aa77' : '#999', fontSize: '0.6em' }}>
|
|
({t ? t("product.inclVat", { vat: ventilation.vat }) : `incl. ${ventilation.vat}% MwSt.,*`})
|
|
</small>
|
|
)}
|
|
</Typography>
|
|
|
|
{/* Selection Indicator */}
|
|
{selectedVentilationType === ventilation.id && ventilation.isDeliverable && (
|
|
<Typography variant="body2" sx={{
|
|
color: '#2e7d32',
|
|
fontWeight: 'bold',
|
|
mt: 1,
|
|
textAlign: 'center'
|
|
}}>
|
|
{t ? t("kitConfig.selected") : "✓ Ausgewählt"}
|
|
</Typography>
|
|
)}
|
|
<Stack direction="row" spacing={1} justifyContent="center">
|
|
<IconButton
|
|
component={Link}
|
|
to={`/Artikel/${ventilation.seoName}`}
|
|
size="small"
|
|
aria-label="Produktdetails anzeigen"
|
|
sx={{ mr: 1, color: 'text.secondary' }}
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<ZoomInIcon />
|
|
</IconButton>
|
|
</Stack>
|
|
</Box>
|
|
</Box>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
renderExtrasSection() {
|
|
const { selectedExtras } = this.state;
|
|
const { t } = this.props;
|
|
|
|
if (!this.state.categoryLoadStatus["Set-zubehoer"]) {
|
|
return (
|
|
<Box sx={{ mb: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectExtrasTitle") : "5. Extras hinzufügen (optional)"}
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
|
{t ? t("kitConfig.loadingExtras") : "Lade Extras..."}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
|
const extrasData = getCachedCategoryData('Set-zubehoer', currentLanguage);
|
|
|
|
if (!extrasData) {
|
|
return (
|
|
<Box sx={{ mb: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectExtrasTitle") : "5. Extras hinzufügen (optional)"}
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
|
{t ? t("kitConfig.noExtrasAvailable") : "Keine Extras verfügbar"}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const extras = Array.isArray(extrasData.products) ? extrasData.products : [];
|
|
|
|
if (extras.length === 0) {
|
|
return (
|
|
<Box sx={{ mb: 4 }}>
|
|
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.selectExtrasTitle") : "5. Extras hinzufügen (optional)"}
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
|
{t ? t("kitConfig.noExtrasAvailable") : "Keine Extras verfügbar"}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ExtrasSelector
|
|
extras={extras}
|
|
selectedExtras={selectedExtras}
|
|
onExtraToggle={this.handleExtraToggle}
|
|
title={t ? t("kitConfig.selectExtrasTitle") : "5. Extras hinzufügen (optional)"}
|
|
gridSize={{ xs: 12, sm: 6, md: 4 }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderInlineSummary() {
|
|
const { selectedTentSize, selectedLightType, selectedVentilationType, selectedExtras, totalPrice } = this.state;
|
|
const { t } = this.props;
|
|
|
|
// 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 availableLamps = this.state.selectedTentShape ? this.getLampsForTentShape(this.state.selectedTentShape) : [];
|
|
const selectedLight = availableLamps.find(l => l.id === selectedLightType);
|
|
const availableVentilation = this.state.selectedTentShape ? this.getVentilationForTentShape(this.state.selectedTentShape) : [];
|
|
const selectedVentilation = availableVentilation.find(v => v.id === selectedVentilationType && v.isDeliverable);
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
|
const extrasData = getCachedCategoryData('Set-zubehoer', currentLanguage);
|
|
const selectedExtrasItems = Array.isArray(extrasData?.products)
|
|
? extrasData.products.filter(e => selectedExtras.includes(e.id))
|
|
: [];
|
|
const savingsInfo = this.calculateSavings();
|
|
|
|
return (
|
|
<Paper
|
|
id="inline-summary" // @note Add ID for scroll targeting
|
|
elevation={2}
|
|
sx={{
|
|
mt: 4,
|
|
p: 3,
|
|
bgcolor: '#f8f9fa',
|
|
border: '2px solid #2e7d32',
|
|
borderRadius: 2
|
|
}}
|
|
>
|
|
<Typography variant="h5" sx={{ color: '#2e7d32', fontWeight: 'bold', mb: 3, textAlign: 'center' }}>
|
|
{t ? t("kitConfig.yourConfiguration") : "🎯 Ihre Konfiguration"}
|
|
</Typography>
|
|
|
|
<List sx={{ '& .MuiListItem-root': { py: 1 } }}>
|
|
{selectedTent && (
|
|
<ListItem>
|
|
<ListItemText
|
|
primary={t ? t("kitConfig.growboxLabel", { name: selectedTent.name }) : `Growbox: ${selectedTent.name}`}
|
|
/>
|
|
<ListItemSecondaryAction>
|
|
<Typography variant="h6" sx={{ fontWeight: 'bold', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
|
<span>{this.formatPrice(selectedTent.price)}</span>
|
|
{selectedTent.vat && (
|
|
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
|
({t ? t("product.inclVat", { vat: selectedTent.vat }) : `incl. ${selectedTent.vat}% MwSt.,*`})
|
|
</small>
|
|
)}
|
|
</Typography>
|
|
</ListItemSecondaryAction>
|
|
</ListItem>
|
|
)}
|
|
|
|
{selectedLight && (
|
|
<ListItem>
|
|
<ListItemText
|
|
primary={t ? t("kitConfig.lightingLabel", { name: selectedLight.name }) : `Beleuchtung: ${selectedLight.name}`}
|
|
/>
|
|
<ListItemSecondaryAction>
|
|
<Typography variant="h6" sx={{ fontWeight: 'bold', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
|
<span>{this.formatPrice(selectedLight.price)}</span>
|
|
{selectedLight.vat && (
|
|
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
|
({t ? t("product.inclVat", { vat: selectedLight.vat }) : `incl. ${selectedLight.vat}% MwSt.,*`})
|
|
</small>
|
|
)}
|
|
</Typography>
|
|
</ListItemSecondaryAction>
|
|
</ListItem>
|
|
)}
|
|
|
|
{selectedVentilation && (
|
|
<ListItem>
|
|
<ListItemText
|
|
primary={t ? t("kitConfig.ventilationLabel", { name: selectedVentilation.name }) : `Belüftung: ${selectedVentilation.name}`}
|
|
/>
|
|
<ListItemSecondaryAction>
|
|
<Typography variant="h6" sx={{ fontWeight: 'bold', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
|
<span>{this.formatPrice(selectedVentilation.price)}</span>
|
|
{selectedVentilation.vat && (
|
|
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
|
({t ? t("product.inclVat", { vat: selectedVentilation.vat }) : `incl. ${selectedVentilation.vat}% MwSt.,*`})
|
|
</small>
|
|
)}
|
|
</Typography>
|
|
</ListItemSecondaryAction>
|
|
</ListItem>
|
|
)}
|
|
|
|
{selectedExtrasItems.map(extra => (
|
|
<ListItem key={extra.id}>
|
|
<ListItemText
|
|
primary={t ? t("kitConfig.extraLabel", { name: extra.name }) : `Extra: ${extra.name}`}
|
|
/>
|
|
<ListItemSecondaryAction>
|
|
<Typography variant="h6" sx={{ fontWeight: 'bold', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
|
<span>{this.formatPrice(extra.price)}</span>
|
|
{extra.vat && (
|
|
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
|
({t ? t("product.inclVat", { vat: extra.vat }) : `incl. ${extra.vat}% MwSt.,*`})
|
|
</small>
|
|
)}
|
|
</Typography>
|
|
</ListItemSecondaryAction>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
|
|
<Divider sx={{ my: 3 }} />
|
|
|
|
{savingsInfo.hasDiscount && (
|
|
<Box sx={{ mb: 2, textAlign: 'center' }}>
|
|
<Typography variant="h6" sx={{ color: '#d32f2f', fontWeight: 'bold' }}>
|
|
{t ? t("product.youSave", { amount: this.formatPrice(savingsInfo.savings) }) : `Sie sparen: ${this.formatPrice(savingsInfo.savings)}`} ({savingsInfo.discountPercentage}% Bundle-Rabatt)
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: '#77aa77' }}>
|
|
({t ? t("product.inclVat", { vat: 19 }) : "incl. 19% MwSt.,*"})
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
|
<Typography variant="h5" sx={{ fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.totalPrice") : "Gesamtpreis:"}
|
|
</Typography>
|
|
<Box sx={{ textAlign: 'right' }}>
|
|
<Typography variant="h5" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{this.formatPrice(totalPrice)}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: '#77aa77' }}>
|
|
({t ? t("product.inclVat", { vat: 19 }) : "incl. 19% MwSt.,*"})
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
|
<Button
|
|
variant="contained"
|
|
size="large"
|
|
onClick={this.handleAddToCart}
|
|
startIcon={<ShoppingCartIcon />}
|
|
sx={{
|
|
bgcolor: '#2e7d32',
|
|
'&:hover': { bgcolor: '#1b5e20' },
|
|
minWidth: 250
|
|
}}
|
|
>
|
|
{t ? t("kitConfig.addToCart") : "In den Warenkorb"}
|
|
</Button>
|
|
</Box>
|
|
</Paper>
|
|
);
|
|
}
|
|
/*
|
|
window.cart.push({
|
|
id: null,
|
|
name: "Set 80x80",
|
|
seoName: nnull,
|
|
pictureList: null,
|
|
price: total,
|
|
fGrundPreis: null,
|
|
cGrundEinheit: null,
|
|
quantity: 1,
|
|
weight: null,
|
|
vat: vat,
|
|
versandklasse: null,
|
|
availableSupplier: null,
|
|
komponenten: setComponents,
|
|
available: null
|
|
});
|
|
*/
|
|
|
|
|
|
render() {
|
|
const { t } = this.props;
|
|
return (
|
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
|
<Paper elevation={2} sx={{ p: 4, borderRadius: 2 }}>
|
|
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
|
<Typography variant="h3" component="h1" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
|
{t ? t("kitConfig.pageTitle") : "🌱 Growbox Konfigurator"}
|
|
</Typography>
|
|
<Typography variant="h6" color="text.secondary">
|
|
{t ? t("kitConfig.pageSubtitle") : "Stelle dein perfektes Indoor Grow Setup zusammen"}
|
|
</Typography>
|
|
|
|
{/* Bundle Discount Information */}
|
|
<Paper
|
|
elevation={1}
|
|
sx={{
|
|
mt: 3,
|
|
p: 2,
|
|
bgcolor: '#f8f9fa',
|
|
border: '1px solid #e9ecef',
|
|
maxWidth: 600,
|
|
mx: 'auto'
|
|
}}
|
|
>
|
|
<Typography variant="h6" sx={{ color: '#2e7d32', fontWeight: 'bold', mb: 2 }}>
|
|
{t ? t("kitConfig.bundleDiscountTitle") : "🎯 Bundle-Rabatt sichern!"}
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-around', flexWrap: 'wrap', gap: 2 }}>
|
|
<Box sx={{ textAlign: 'center' }}>
|
|
<Typography variant="h6" sx={{ color: '#1976d2', fontWeight: 'bold' }}>
|
|
15%
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
{/* Note: Translation key would be: product.discount.from3Products */}
|
|
{t ? t("product.discount.from3Products") : "ab 3 Produkten"}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ textAlign: 'center' }}>
|
|
<Typography variant="h6" sx={{ color: '#ed6c02', fontWeight: 'bold' }}>
|
|
22%
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
{/* Note: Translation key would be: product.discount.from5Products */}
|
|
{t ? t("product.discount.from5Products") : "ab 5 Produkten"}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ textAlign: 'center' }}>
|
|
<Typography variant="h6" sx={{ color: '#d32f2f', fontWeight: 'bold' }}>
|
|
28%
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
{/* Note: Translation key would be: product.discount.from7Products */}
|
|
{t ? t("product.discount.from7Products") : "ab 7 Produkten"}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
<Typography variant="caption" sx={{ color: '#666', mt: 1, display: 'block' }}>
|
|
{/* Note: Translation key would be: product.discount.moreProductsMoreSavings */}
|
|
{t ? t("product.discount.moreProductsMoreSavings") : "Je mehr Produkte du auswählst, desto mehr sparst du!"}
|
|
</Typography>
|
|
</Paper>
|
|
</Box>
|
|
|
|
{this.renderTentShapeSection()}
|
|
<Divider sx={{ my: 4 }} />
|
|
{this.renderTentSizeSection()}
|
|
{this.state.selectedTentShape && <Divider sx={{ my: 4 }} />}
|
|
|
|
{this.renderLightSection()}
|
|
<Divider sx={{ my: 4 }} />
|
|
|
|
{this.renderVentilationSection()}
|
|
<Divider sx={{ my: 4 }} />
|
|
|
|
{this.renderExtrasSection()}
|
|
|
|
{/* Inline summary section - expands when scrolling to bottom */}
|
|
{this.renderInlineSummary()}
|
|
</Paper>
|
|
</Container>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default withI18n()(GrowTentKonfigurator);
|