Compare commits

...

2 Commits

Author SHA1 Message Date
sebseb7
cbb8dc463f feat: enhance ExtrasSelector and GrowTentKonfigurator for improved extras handling and UI
- Refactored ExtrasSelector to implement dynamic image loading with caching, improving performance and user experience.
- Updated GrowTentKonfigurator to fetch and display extras from a new category, ensuring accurate pricing and availability.
- Enhanced UI elements for better layout and clarity, including loading indicators and improved styling for extras display.
- Added handling for cases when no extras are available, providing clear feedback to users.
2025-09-04 10:45:55 +02:00
sebseb7
479e328e7c feat: update ExtrasSelector and GrowTentKonfigurator for VAT display and extras handling
- Refactored ExtrasSelector to include VAT information for each extra, enhancing clarity for users.
- Removed unused extras data from configuratorData.js to streamline the codebase.
- Updated GrowTentKonfigurator to dynamically retrieve extras from cached data, ensuring accurate pricing and VAT display for selected items.
- Improved UI layout for price and VAT information across various components for better user experience.
2025-09-04 10:31:59 +02:00
3 changed files with 227 additions and 180 deletions

View File

@@ -1,12 +1,9 @@
import React, { Component } from 'react';
import Grid from '@mui/material/Grid';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import CircularProgress from '@mui/material/CircularProgress';
class ExtrasSelector extends Component {
formatPrice(price) {
@@ -16,124 +13,159 @@ class ExtrasSelector extends Component {
}).format(price);
}
// Render product image using working code from GrowTentKonfigurator
renderProductImage(product) {
if (!window.smallPicCache) {
window.smallPicCache = {};
}
const pictureList = product.pictureList;
if (!pictureList || pictureList.length === 0 || !pictureList.split(',').length) {
return (
<CardMedia
component="img"
height="160"
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="160"
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 (
<Box sx={{ height: '160px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<CircularProgress sx={{ color: '#90ffc0' }} />
</Box>
);
}
renderExtraCard(extra) {
const { selectedExtras, onExtraToggle, showImage = true } = this.props;
const isSelected = selectedExtras.includes(extra.id);
return (
<Card
key={extra.id}
sx={{
<Box sx={{
width: { xs: '100%', sm: '250px' },
height: '100%',
display: 'flex',
flexDirection: 'column',
borderRadius: '8px',
overflow: 'hidden',
cursor: 'pointer',
border: '2px solid',
borderColor: isSelected ? '#2e7d32' : '#e0e0e0',
backgroundColor: isSelected ? '#f1f8e9' : '#ffffff',
'&:hover': {
boxShadow: 5,
boxShadow: 6,
borderColor: isSelected ? '#2e7d32' : '#90caf9'
},
transition: 'all 0.3s ease',
cursor: 'pointer'
transition: 'all 0.3s ease'
}}
onClick={() => onExtraToggle(extra.id)}
>
onClick={() => onExtraToggle(extra.id)}>
{/* Image */}
{showImage && (
<CardMedia
component="img"
height="160"
image={extra.image}
alt={extra.name}
sx={{ objectFit: 'cover' }}
/>
)}
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 2 }}>
<FormControlLabel
control={
<Checkbox
checked={isSelected}
onChange={(e) => {
e.stopPropagation();
onExtraToggle(extra.id);
}}
sx={{
color: '#2e7d32',
'&.Mui-checked': { color: '#2e7d32' },
padding: 0
}}
/>
}
label=""
sx={{ margin: 0 }}
onClick={(e) => e.stopPropagation()}
/>
<Typography variant="h6" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
{this.formatPrice(extra.price)}
</Typography>
<Box sx={{
height: { xs: '240px', sm: '180px' },
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff'
}}>
{this.renderProductImage(extra)}
</Box>
)}
{/* Content */}
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
{/* Name */}
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>
{extra.name}
</Typography>
<Typography variant="body2" color="text.secondary">
{extra.description}
{/* Price with VAT - Same as other sections */}
<Typography variant="h6" sx={{
color: '#2e7d32',
fontWeight: 'bold',
mt: 2,
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap'
}}>
<span>{extra.price ? this.formatPrice(extra.price) : 'Kein Preis'}</span>
{extra.vat && (
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
(incl. {extra.vat}% MwSt.,*)
</small>
)}
</Typography>
{/* Selection Indicator - Separate line */}
{isSelected && (
<Box sx={{ mt: 2, textAlign: 'center' }}>
<Typography variant="body2" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
Hinzugefügt
<Typography variant="body2" sx={{
color: '#2e7d32',
fontWeight: 'bold',
mt: 1,
textAlign: 'center'
}}>
Ausgewählt
</Typography>
</Box>
)}
</CardContent>
</Card>
</Box>
</Box>
);
}
render() {
const { extras, title, subtitle, groupByCategory = true, gridSize = { xs: 12, sm: 6, md: 4 } } = this.props;
if (groupByCategory) {
// Group extras by category
const groupedExtras = extras.reduce((acc, extra) => {
if (!acc[extra.category]) {
acc[extra.category] = [];
}
acc[extra.category].push(extra);
return acc;
}, {});
const { extras, title, subtitle, gridSize = { xs: 12, sm: 6, md: 4 } } = this.props;
if (!extras || !Array.isArray(extras)) {
return (
<Box sx={{ mb: 4 }}>
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
{title}
</Typography>
{subtitle && (
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
{subtitle}
Keine Extras verfügbar
</Typography>
)}
{Object.entries(groupedExtras).map(([category, categoryExtras]) => (
<Box key={category} sx={{ mb: 3 }}>
<Typography variant="h6" gutterBottom sx={{ color: '#1976d2' }}>
{category}
</Typography>
<Grid container spacing={2}>
{categoryExtras.map(extra => (
<Grid item {...gridSize} key={extra.id}>
{this.renderExtraCard(extra)}
</Grid>
))}
</Grid>
</Box>
))}
</Box>
);
}
// Render without category grouping
return (
<Box sx={{ mb: 4 }}>
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>

View File

@@ -41,54 +41,3 @@ export const tentShapes = [
visualDepth: 60
}
];
export const extras = [
{
id: 'ph_tester',
name: 'pH-Messgerät',
description: 'Digitales pH-Meter',
price: 29.99,
image: '/assets/images/nopicture.jpg',
category: 'Messung'
},
{
id: 'nutrients_starter',
name: 'Dünger Starter-Set',
description: 'Komplettes Nährstoff-Set',
price: 39.99,
image: '/assets/images/nopicture.jpg',
category: 'Nährstoffe'
},
{
id: 'grow_pots',
name: 'Grow-Töpfe Set (5x)',
description: '5x Stofftöpfe 11L',
price: 24.99,
image: '/assets/images/nopicture.jpg',
category: 'Töpfe'
},
{
id: 'timer_socket',
name: 'Zeitschaltuhr',
description: 'Digitale Zeitschaltuhr',
price: 19.99,
image: '/assets/images/nopicture.jpg',
category: 'Steuerung'
},
{
id: 'thermometer',
name: 'Thermo-Hygrometer',
description: 'Min/Max Temperatur & Luftfeuchtigkeit',
price: 14.99,
image: '/assets/images/nopicture.jpg',
category: 'Messung'
},
{
id: 'pruning_shears',
name: 'Gartenschere',
description: 'Präzisions-Gartenschere',
price: 16.99,
image: '/assets/images/nopicture.jpg',
category: 'Werkzeug'
}
];

View File

@@ -16,7 +16,7 @@ import {
} from '@mui/material';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { TentShapeSelector, ExtrasSelector } from '../components/configurator/index.js';
import { tentShapes, extras } from '../data/configuratorData.js';
import { tentShapes } from '../data/configuratorData.js';
function setCachedCategoryData(categoryId, data) {
if (!window.productCache) {
@@ -109,6 +109,7 @@ class GrowTentKonfigurator extends Component {
this.fetchCategoryData("Zelte");
this.fetchCategoryData("Lampen");
this.fetchCategoryData("Abluft-sets");
this.fetchCategoryData("Set-zubehoer");
}
componentDidUpdate(prevProps, prevState) {
@@ -497,14 +498,19 @@ class GrowTentKonfigurator extends Component {
}
}
const extrasData = getCachedCategoryData('Set-zubehoer');
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 = extras.find(e => e.id === extraId);
const extra = extrasData.products.find(e => e.id === extraId);
if (extra) {
total += extra.price;
itemCount++;
}
});
}
// Apply bundle discount
let discountPercentage = 0;
@@ -570,13 +576,18 @@ class GrowTentKonfigurator extends Component {
}
}
const extrasData = getCachedCategoryData('Set-zubehoer');
if (!extrasData || !Array.isArray(extrasData.products)) {
console.warn('Extras data not available; skipping extras in savings calculation');
} else {
selectedExtras.forEach(extraId => {
const extra = extras.find(e => e.id === 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;
@@ -1034,6 +1045,35 @@ class GrowTentKonfigurator extends Component {
renderExtrasSection() {
const { selectedExtras } = this.state;
const extrasData = getCachedCategoryData('Set-zubehoer');
if (!extrasData) {
return (
<Box sx={{ mb: 4 }}>
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
5. Extras hinzufügen (optional)
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
Lade Extras...
</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' }}>
5. Extras hinzufügen (optional)
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
Keine Extras verfügbar
</Typography>
</Box>
);
}
return (
<ExtrasSelector
@@ -1041,7 +1081,6 @@ class GrowTentKonfigurator extends Component {
selectedExtras={selectedExtras}
onExtraToggle={this.handleExtraToggle}
title="5. Extras hinzufügen (optional)"
groupByCategory={true}
gridSize={{ xs: 12, sm: 6, md: 4 }}
/>
);
@@ -1062,7 +1101,10 @@ class GrowTentKonfigurator extends Component {
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 selectedExtrasItems = extras.filter(e => selectedExtras.includes(e.id));
const extrasData = getCachedCategoryData('Set-zubehoer');
const selectedExtrasItems = Array.isArray(extrasData?.products)
? extrasData.products.filter(e => selectedExtras.includes(e.id))
: [];
const savingsInfo = this.calculateSavings();
return (
@@ -1086,11 +1128,15 @@ class GrowTentKonfigurator extends Component {
<ListItem>
<ListItemText
primary={`Growbox: ${selectedTent.name}`}
secondary={selectedTent.description}
/>
<ListItemSecondaryAction>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{this.formatPrice(selectedTent.price)}
<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' }}>
(incl. {selectedTent.vat}% MwSt.,*)
</small>
)}
</Typography>
</ListItemSecondaryAction>
</ListItem>
@@ -1100,11 +1146,15 @@ class GrowTentKonfigurator extends Component {
<ListItem>
<ListItemText
primary={`Beleuchtung: ${selectedLight.name}`}
secondary={selectedLight.description}
/>
<ListItemSecondaryAction>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{this.formatPrice(selectedLight.price)}
<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' }}>
(incl. {selectedLight.vat}% MwSt.,*)
</small>
)}
</Typography>
</ListItemSecondaryAction>
</ListItem>
@@ -1114,11 +1164,15 @@ class GrowTentKonfigurator extends Component {
<ListItem>
<ListItemText
primary={`Belüftung: ${selectedVentilation.name}`}
secondary={selectedVentilation.description}
/>
<ListItemSecondaryAction>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{this.formatPrice(selectedVentilation.price)}
<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' }}>
(incl. {selectedVentilation.vat}% MwSt.,*)
</small>
)}
</Typography>
</ListItemSecondaryAction>
</ListItem>
@@ -1128,11 +1182,15 @@ class GrowTentKonfigurator extends Component {
<ListItem key={extra.id}>
<ListItemText
primary={`Extra: ${extra.name}`}
secondary={extra.description}
/>
<ListItemSecondaryAction>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{this.formatPrice(extra.price)}
<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' }}>
(incl. {extra.vat}% MwSt.,*)
</small>
)}
</Typography>
</ListItemSecondaryAction>
</ListItem>
@@ -1146,6 +1204,9 @@ class GrowTentKonfigurator extends Component {
<Typography variant="h6" sx={{ color: '#d32f2f', fontWeight: 'bold' }}>
Sie sparen: {this.formatPrice(savingsInfo.savings)} ({savingsInfo.discountPercentage}% Bundle-Rabatt)
</Typography>
<Typography variant="caption" sx={{ color: '#77aa77' }}>
(incl. 19% MwSt.,*)
</Typography>
</Box>
)}
@@ -1153,9 +1214,14 @@ class GrowTentKonfigurator extends Component {
<Typography variant="h5" sx={{ fontWeight: 'bold' }}>
Gesamtpreis:
</Typography>
<Box sx={{ textAlign: 'right' }}>
<Typography variant="h5" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
{this.formatPrice(totalPrice)}
</Typography>
<Typography variant="caption" sx={{ color: '#77aa77' }}>
(incl. 19% MwSt.,*)
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>