Genesis
This commit is contained in:
159
src/components/configurator/ExtrasSelector.js
Normal file
159
src/components/configurator/ExtrasSelector.js
Normal file
@@ -0,0 +1,159 @@
|
||||
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';
|
||||
|
||||
class ExtrasSelector extends Component {
|
||||
formatPrice(price) {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(price);
|
||||
}
|
||||
|
||||
renderExtraCard(extra) {
|
||||
const { selectedExtras, onExtraToggle, showImage = true } = this.props;
|
||||
const isSelected = selectedExtras.includes(extra.id);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={extra.id}
|
||||
sx={{
|
||||
height: '100%',
|
||||
border: '2px solid',
|
||||
borderColor: isSelected ? '#2e7d32' : '#e0e0e0',
|
||||
backgroundColor: isSelected ? '#f1f8e9' : '#ffffff',
|
||||
'&:hover': {
|
||||
boxShadow: 5,
|
||||
borderColor: isSelected ? '#2e7d32' : '#90caf9'
|
||||
},
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => onExtraToggle(extra.id)}
|
||||
>
|
||||
{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>
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||
{extra.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{extra.description}
|
||||
</Typography>
|
||||
|
||||
{isSelected && (
|
||||
<Box sx={{ mt: 2, textAlign: 'center' }}>
|
||||
<Typography variant="body2" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
✓ Hinzugefügt
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h5" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle && (
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{subtitle}
|
||||
</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="h5" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle && (
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
<Grid container spacing={2}>
|
||||
{extras.map(extra => (
|
||||
<Grid item {...gridSize} key={extra.id}>
|
||||
{this.renderExtraCard(extra)}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ExtrasSelector;
|
||||
170
src/components/configurator/ProductSelector.js
Normal file
170
src/components/configurator/ProductSelector.js
Normal file
@@ -0,0 +1,170 @@
|
||||
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 Chip from '@mui/material/Chip';
|
||||
|
||||
class ProductSelector extends Component {
|
||||
formatPrice(price) {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(price);
|
||||
}
|
||||
|
||||
renderProductCard(product) {
|
||||
const { selectedValue, onSelect, showImage = true } = this.props;
|
||||
const isSelected = selectedValue === product.id;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={product.id}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
border: '2px solid',
|
||||
borderColor: isSelected ? '#2e7d32' : '#e0e0e0',
|
||||
backgroundColor: isSelected ? '#f1f8e9' : '#ffffff',
|
||||
'&:hover': {
|
||||
boxShadow: 6,
|
||||
borderColor: isSelected ? '#2e7d32' : '#90caf9'
|
||||
},
|
||||
transition: 'all 0.3s ease',
|
||||
height: '100%'
|
||||
}}
|
||||
onClick={() => onSelect(product.id)}
|
||||
>
|
||||
{showImage && (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="180"
|
||||
image={product.image}
|
||||
alt={product.name}
|
||||
sx={{ objectFit: 'cover' }}
|
||||
/>
|
||||
)}
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||
{product.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{product.description}
|
||||
</Typography>
|
||||
|
||||
{/* Product specific information */}
|
||||
{this.renderProductDetails(product)}
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
|
||||
<Typography variant="h6" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
{this.formatPrice(product.price)}
|
||||
</Typography>
|
||||
{isSelected && (
|
||||
<Typography variant="body2" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
✓ Ausgewählt
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
renderProductDetails(product) {
|
||||
const { productType } = this.props;
|
||||
|
||||
switch (productType) {
|
||||
case 'tent':
|
||||
return (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>Maße:</strong> {product.dimensions}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<strong>Für:</strong> {product.coverage}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 'light':
|
||||
return (
|
||||
<Box sx={{ mt: 2, mb: 2 }}>
|
||||
<Chip
|
||||
label={product.wattage}
|
||||
size="small"
|
||||
sx={{ mr: 1, mb: 1, pointerEvents: 'none' }}
|
||||
/>
|
||||
<Chip
|
||||
label={product.coverage}
|
||||
size="small"
|
||||
sx={{ mr: 1, mb: 1, pointerEvents: 'none' }}
|
||||
/>
|
||||
<Chip
|
||||
label={product.spectrum}
|
||||
size="small"
|
||||
sx={{ mr: 1, mb: 1, pointerEvents: 'none' }}
|
||||
/>
|
||||
<Chip
|
||||
label={`Effizienz: ${product.efficiency}`}
|
||||
size="small"
|
||||
sx={{ mb: 1, pointerEvents: 'none' }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 'ventilation':
|
||||
return (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>Luftdurchsatz:</strong> {product.airflow}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<strong>Lautstärke:</strong> {product.noiseLevel}
|
||||
</Typography>
|
||||
{product.includes && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Beinhaltet:</strong>
|
||||
</Typography>
|
||||
{product.includes.map((item, index) => (
|
||||
<Typography key={index} variant="body2" sx={{ fontSize: '0.8rem' }}>
|
||||
• {item}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { products, title, subtitle, gridSize = { xs: 12, sm: 6, md: 4 } } = this.props;
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h5" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle && (
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
<Grid container spacing={2}>
|
||||
{products.map(product => (
|
||||
<Grid item {...gridSize} key={product.id}>
|
||||
{this.renderProductCard(product)}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductSelector;
|
||||
241
src/components/configurator/TentShapeSelector.js
Normal file
241
src/components/configurator/TentShapeSelector.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import React, { Component } from 'react';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
|
||||
class TentShapeSelector extends Component {
|
||||
// Generate plant layout based on tent shape
|
||||
generatePlantLayout(shapeId) {
|
||||
const layouts = {
|
||||
'60x60': [
|
||||
{ x: 50, y: 50, size: 18 } // 1 large plant centered
|
||||
],
|
||||
'80x80': [
|
||||
{ x: 35, y: 35, size: 12 }, // 2x2 = 4 plants
|
||||
{ x: 65, y: 35, size: 12 },
|
||||
{ x: 35, y: 65, size: 12 },
|
||||
{ x: 65, y: 65, size: 12 }
|
||||
],
|
||||
'100x100': [
|
||||
{ x: 22, y: 22, size: 10 }, // 3x3 = 9 plants
|
||||
{ x: 50, y: 22, size: 10 },
|
||||
{ x: 78, y: 22, size: 10 },
|
||||
{ x: 22, y: 50, size: 10 },
|
||||
{ x: 50, y: 50, size: 10 },
|
||||
{ x: 78, y: 50, size: 10 },
|
||||
{ x: 22, y: 78, size: 10 },
|
||||
{ x: 50, y: 78, size: 10 },
|
||||
{ x: 78, y: 78, size: 10 }
|
||||
],
|
||||
'120x60': [
|
||||
{ x: 30, y: 50, size: 14 }, // 1x3 = 3 larger plants
|
||||
{ x: 50, y: 50, size: 14 },
|
||||
{ x: 70, y: 50, size: 14 }
|
||||
]
|
||||
};
|
||||
|
||||
return layouts[shapeId] || [];
|
||||
}
|
||||
|
||||
renderShapeCard(shape) {
|
||||
const { selectedShape, onShapeSelect } = this.props;
|
||||
const isSelected = selectedShape === shape.id;
|
||||
|
||||
const plants = this.generatePlantLayout(shape.id);
|
||||
|
||||
// Make visual sizes proportional to actual dimensions
|
||||
let visualWidth, visualHeight;
|
||||
switch(shape.id) {
|
||||
case '60x60':
|
||||
visualWidth = 90;
|
||||
visualHeight = 90;
|
||||
break;
|
||||
case '80x80':
|
||||
visualWidth = 110;
|
||||
visualHeight = 110;
|
||||
break;
|
||||
case '100x100':
|
||||
visualWidth = 130;
|
||||
visualHeight = 130;
|
||||
break;
|
||||
case '120x60':
|
||||
visualWidth = 140;
|
||||
visualHeight = 80;
|
||||
break;
|
||||
default:
|
||||
visualWidth = 120;
|
||||
visualHeight = 120;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={shape.id}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
border: '3px solid',
|
||||
borderColor: isSelected ? '#2e7d32' : '#e0e0e0',
|
||||
backgroundColor: isSelected ? '#f1f8e9' : '#ffffff',
|
||||
'&:hover': {
|
||||
boxShadow: 8,
|
||||
borderColor: isSelected ? '#2e7d32' : '#90caf9',
|
||||
transform: 'translateY(-2px)'
|
||||
},
|
||||
transition: 'all 0.3s ease',
|
||||
height: '100%',
|
||||
minHeight: 300
|
||||
}}
|
||||
onClick={() => onShapeSelect(shape.id)}
|
||||
>
|
||||
<CardContent sx={{ textAlign: 'center', p: 3 }}>
|
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||
{shape.name}
|
||||
</Typography>
|
||||
|
||||
{/* Enhanced visual representation with plant layout */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: visualHeight,
|
||||
mb: 2,
|
||||
position: 'relative'
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
width: `${visualWidth}px`,
|
||||
height: `${visualHeight}px`,
|
||||
border: '3px solid #2e7d32',
|
||||
borderRadius: 2,
|
||||
backgroundColor: isSelected ? '#e8f5e8' : '#f5f5f5',
|
||||
position: 'relative',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* Grid pattern */}
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ position: 'absolute', top: 0, left: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<pattern id={`grid-${shape.id}`} width="10" height="10" patternUnits="userSpaceOnUse">
|
||||
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="#e0e0e0" strokeWidth="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill={`url(#grid-${shape.id})`} />
|
||||
|
||||
{/* Plants */}
|
||||
{plants.map((plant, index) => (
|
||||
<circle
|
||||
key={index}
|
||||
cx={`${plant.x}%`}
|
||||
cy={`${plant.y}%`}
|
||||
r={plant.size}
|
||||
fill="#4caf50"
|
||||
fillOpacity="0.8"
|
||||
stroke="#2e7d32"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
{/* Dimensions label */}
|
||||
<Typography variant="caption" sx={{
|
||||
position: 'absolute',
|
||||
bottom: 4,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
fontWeight: 'bold',
|
||||
color: '#2e7d32',
|
||||
backgroundColor: 'rgba(255,255,255,0.8)',
|
||||
px: 1,
|
||||
borderRadius: 1,
|
||||
fontSize: '11px'
|
||||
}}>
|
||||
{shape.footprint}
|
||||
</Typography>
|
||||
|
||||
{/* Plant count label */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
right: 4,
|
||||
backgroundColor: 'rgba(46, 125, 50, 0.9)',
|
||||
color: 'white',
|
||||
px: 1,
|
||||
borderRadius: 1,
|
||||
fontSize: '10px',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{plants.length} 🌱
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{shape.description}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Chip
|
||||
label={`${shape.minPlants}-${shape.maxPlants} Pflanzen`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: isSelected ? '#2e7d32' : '#f0f0f0',
|
||||
color: isSelected ? 'white' : 'inherit',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 2, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: '#2e7d32',
|
||||
fontWeight: 'bold',
|
||||
opacity: isSelected ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease'
|
||||
}}
|
||||
>
|
||||
✓ Ausgewählt
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tentShapes, title, subtitle } = this.props;
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h5" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle && (
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
<Grid container spacing={3}>
|
||||
{tentShapes.map(shape => (
|
||||
<Grid item xs={12} sm={6} md={3} key={shape.id}>
|
||||
{this.renderShapeCard(shape)}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TentShapeSelector;
|
||||
3
src/components/configurator/index.js
Normal file
3
src/components/configurator/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as TentShapeSelector } from './TentShapeSelector.js';
|
||||
export { default as ProductSelector } from './ProductSelector.js';
|
||||
export { default as ExtrasSelector } from './ExtrasSelector.js';
|
||||
Reference in New Issue
Block a user