650 lines
22 KiB
JavaScript
650 lines
22 KiB
JavaScript
import React, { Component } from 'react';
|
|
import Box from '@mui/material/Box';
|
|
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 CircularProgress from '@mui/material/CircularProgress';
|
|
import IconButton from '@mui/material/IconButton';
|
|
import AddToCartButton from './AddToCartButton.js';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import { withI18n } from '../i18n/withTranslation.js';
|
|
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
|
import { STAR_POLYGON_POINTS } from '../utils/starPolygon.js';
|
|
import {
|
|
PRODUCT_CARD_MOBILE_MAX_WIDTH_PX,
|
|
PRODUCT_CARD_WIDTH_SM_PX,
|
|
PRODUCT_CARD_WIDTH_XS_PX,
|
|
} from '../utils/productCardLayout.js';
|
|
|
|
// Helper function to find level 1 category ID from any category ID
|
|
const findLevel1CategoryId = (categoryId) => {
|
|
try {
|
|
const currentLanguage = 'de'; // Default to German
|
|
const categoryTreeCache = window.categoryService?.getSync(209, currentLanguage);
|
|
|
|
if (!categoryTreeCache || !categoryTreeCache.children) {
|
|
return null;
|
|
}
|
|
|
|
// Helper function to find category by ID and get its level 1 parent
|
|
const findCategoryAndLevel1 = (categories, targetId) => {
|
|
for (const category of categories) {
|
|
if (category.id === targetId) {
|
|
// Found the category, now find its level 1 parent
|
|
return findLevel1Parent(categoryTreeCache.children, category);
|
|
}
|
|
|
|
if (category.children && category.children.length > 0) {
|
|
const result = findCategoryAndLevel1(category.children, targetId);
|
|
if (result) return result;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Helper function to find the level 1 parent (direct child of root category 209)
|
|
const findLevel1Parent = (level1Categories, category) => {
|
|
// If this category's parent is 209, it's already level 1
|
|
if (category.parentId === 209) {
|
|
return category.id;
|
|
}
|
|
|
|
// Otherwise, find the parent and check if it's level 1
|
|
for (const level1Category of level1Categories) {
|
|
if (level1Category.id === category.parentId) {
|
|
return level1Category.id;
|
|
}
|
|
|
|
// If parent has children, search recursively
|
|
if (level1Category.children && level1Category.children.length > 0) {
|
|
const result = findLevel1Parent(level1Category.children, category);
|
|
if (result) return result;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
return findCategoryAndLevel1(categoryTreeCache.children, parseInt(categoryId));
|
|
|
|
} catch (error) {
|
|
console.error('Error finding level 1 category:', error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
class Product extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this._isMounted = false;
|
|
|
|
if (!window.smallPicCache) {
|
|
window.smallPicCache = {};
|
|
}
|
|
|
|
const pictureIds = (this.props.pictureList && this.props.pictureList.length > 0)
|
|
? this.props.pictureList.split(',').filter(id => id.trim().length > 0)
|
|
: [];
|
|
|
|
if (pictureIds.length > 0) {
|
|
const initialImages = pictureIds.map(id => window.smallPicCache[id] || null);
|
|
const isFirstCached = !!initialImages[0];
|
|
|
|
this.state = {
|
|
images: initialImages,
|
|
currentImageIndex: 0,
|
|
loading: !isFirstCached,
|
|
error: false,
|
|
isHovering: false
|
|
};
|
|
|
|
pictureIds.forEach((id, index) => {
|
|
if (!window.smallPicCache[id]) {
|
|
this.loadImage(id, index);
|
|
}
|
|
});
|
|
} else {
|
|
this.state = { images: [], currentImageIndex: 0, loading: false, error: false, isHovering: false };
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
this._isMounted = true;
|
|
this.startRandomFading();
|
|
}
|
|
|
|
startRandomFading = () => {
|
|
if (this.state.isHovering) return;
|
|
|
|
const pictureIds = (this.props.pictureList && this.props.pictureList.length > 0)
|
|
? this.props.pictureList.split(',').filter(id => id.trim().length > 0)
|
|
: [];
|
|
|
|
if (pictureIds.length > 1) {
|
|
const minInterval = 4000;
|
|
const maxInterval = 8000;
|
|
const randomInterval = Math.floor(Math.random() * (maxInterval - minInterval + 1)) + minInterval;
|
|
|
|
this.fadeTimeout = setTimeout(() => {
|
|
if (this._isMounted) {
|
|
this.setState(prevState => {
|
|
let nextIndex = (prevState.currentImageIndex + 1) % pictureIds.length;
|
|
let attempts = 0;
|
|
while (!prevState.images[nextIndex] && attempts < pictureIds.length) {
|
|
nextIndex = (nextIndex + 1) % pictureIds.length;
|
|
attempts++;
|
|
}
|
|
if (attempts < pictureIds.length && nextIndex !== prevState.currentImageIndex) {
|
|
return { currentImageIndex: nextIndex };
|
|
}
|
|
return null;
|
|
}, () => {
|
|
this.startRandomFading();
|
|
});
|
|
}
|
|
}, randomInterval);
|
|
}
|
|
}
|
|
|
|
handleMouseMove = (e) => {
|
|
const pictureIds = (this.props.pictureList && this.props.pictureList.length > 0)
|
|
? this.props.pictureList.split(',').filter(id => id.trim().length > 0)
|
|
: [];
|
|
|
|
if (pictureIds.length > 1) {
|
|
if (this.fadeTimeout) {
|
|
clearTimeout(this.fadeTimeout);
|
|
this.fadeTimeout = null;
|
|
}
|
|
|
|
const { left, width } = e.currentTarget.getBoundingClientRect();
|
|
const x = e.clientX - left;
|
|
|
|
const segmentWidth = width / pictureIds.length;
|
|
let targetIndex = Math.floor(x / segmentWidth);
|
|
if (targetIndex >= pictureIds.length) targetIndex = pictureIds.length - 1;
|
|
if (targetIndex < 0) targetIndex = 0;
|
|
|
|
if (this.state.currentImageIndex !== targetIndex) {
|
|
if (this.state.images[targetIndex]) {
|
|
this.setState({ currentImageIndex: targetIndex, isHovering: true });
|
|
} else {
|
|
this.setState({ isHovering: true });
|
|
}
|
|
} else if (!this.state.isHovering) {
|
|
this.setState({ isHovering: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
handleMouseLeave = () => {
|
|
if (this.state.isHovering) {
|
|
this.setState({ isHovering: false }, () => {
|
|
this.startRandomFading();
|
|
});
|
|
}
|
|
}
|
|
|
|
loadImage = (bildId, index) => {
|
|
console.log('loadImagevisSocket', bildId);
|
|
window.socketManager.emit('getPic', { bildId, size: 'small' }, (res) => {
|
|
if (res.success) {
|
|
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
|
|
if (this._isMounted) {
|
|
this.setState(prevState => {
|
|
const newImages = [...prevState.images];
|
|
newImages[index] = window.smallPicCache[bildId];
|
|
return {
|
|
images: newImages,
|
|
loading: index === 0 ? false : prevState.loading
|
|
};
|
|
});
|
|
} else {
|
|
this.state.images[index] = window.smallPicCache[bildId];
|
|
if (index === 0) this.state.loading = false;
|
|
}
|
|
} else {
|
|
console.log('Fehler beim Laden des Bildes:', res);
|
|
if (this._isMounted) {
|
|
this.setState(prevState => ({
|
|
error: index === 0 ? true : prevState.error,
|
|
loading: index === 0 ? false : prevState.loading
|
|
}));
|
|
} else {
|
|
if (index === 0) {
|
|
this.state.error = true;
|
|
this.state.loading = false;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
componentWillUnmount() {
|
|
this._isMounted = false;
|
|
if (this.fadeTimeout) {
|
|
clearTimeout(this.fadeTimeout);
|
|
}
|
|
}
|
|
|
|
handleQuantityChange = (quantity) => {
|
|
console.log(`Product: ${this.props.name}, Quantity: ${quantity}`);
|
|
// In a real app, this would update a cart state in a parent component or Redux store
|
|
}
|
|
|
|
handleProductClick = (e) => {
|
|
e.preventDefault();
|
|
|
|
const { categoryId } = this.props;
|
|
|
|
// Find the level 1 category for this product
|
|
const level1CategoryId = categoryId ? findLevel1CategoryId(categoryId) : null;
|
|
|
|
// Navigate to the product page WITH the category information in the state
|
|
const navigate = this.props.navigate;
|
|
if (navigate) {
|
|
navigate(`/Artikel/${this.props.seoName}`, {
|
|
state: { articleCategoryId: level1CategoryId }
|
|
});
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
id, name, price, available, manufacturer, seoName,
|
|
currency, vat, cGrundEinheit, fGrundPreis, thc,
|
|
floweringWeeks, incoming, neu, weight, versandklasse, availableSupplier, komponenten
|
|
} = this.props;
|
|
|
|
const isNew = neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
|
const showThcBadge = thc > 0;
|
|
let thcBadgeColor = '#4caf50'; // Green default
|
|
if (thc > 30) {
|
|
thcBadgeColor = '#f44336'; // Red for > 30
|
|
} else if (thc > 25) {
|
|
thcBadgeColor = '#ffeb3b'; // Yellow for > 25
|
|
}
|
|
const showFloweringWeeksBadge = floweringWeeks > 0;
|
|
let floweringWeeksBadgeColor = '#4caf50'; // Green default
|
|
if (floweringWeeks > 12) {
|
|
floweringWeeksBadgeColor = '#f44336'; // Red for > 12
|
|
} else if (floweringWeeks > 8) {
|
|
floweringWeeksBadgeColor = '#ffeb3b'; // Yellow for > 8
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
<Box sx={{
|
|
position: 'relative',
|
|
height: '100%',
|
|
/* Match card width on xs so absolute NEU star is relative to the card, not the full grid row */
|
|
width: {
|
|
xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`,
|
|
sm: 'auto',
|
|
},
|
|
minWidth: { xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`, sm: 'auto' },
|
|
maxWidth: { xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`, sm: 'none' },
|
|
display: 'flex',
|
|
justifyContent: { xs: 'center', sm: 'flex-start' },
|
|
mx: { xs: 'auto', sm: 0 },
|
|
}}>
|
|
{isNew && (
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '-15px',
|
|
right: '-15px',
|
|
width: '60px',
|
|
height: '60px',
|
|
zIndex: 999,
|
|
pointerEvents: 'none'
|
|
}}
|
|
>
|
|
{/* Background star - slightly larger and rotated */}
|
|
<svg
|
|
viewBox="0 0 60 60"
|
|
width="56"
|
|
height="56"
|
|
style={{
|
|
position: 'absolute',
|
|
top: '-3px',
|
|
left: '-3px',
|
|
transform: 'rotate(20deg)'
|
|
}}
|
|
>
|
|
<polygon
|
|
points={STAR_POLYGON_POINTS}
|
|
fill="#20403a"
|
|
stroke="none"
|
|
/>
|
|
</svg>
|
|
|
|
{/* Middle star - medium size with different rotation */}
|
|
<svg
|
|
viewBox="0 0 60 60"
|
|
width="53"
|
|
height="53"
|
|
style={{
|
|
position: 'absolute',
|
|
top: '-1.5px',
|
|
left: '-1.5px',
|
|
transform: 'rotate(-25deg)'
|
|
}}
|
|
>
|
|
<polygon
|
|
points={STAR_POLYGON_POINTS}
|
|
fill="#40736b"
|
|
stroke="none"
|
|
/>
|
|
</svg>
|
|
|
|
{/* Foreground star - main star with text */}
|
|
<svg
|
|
viewBox="0 0 60 60"
|
|
width="50"
|
|
height="50"
|
|
>
|
|
<polygon
|
|
points={STAR_POLYGON_POINTS}
|
|
fill="#609688"
|
|
stroke="none"
|
|
/>
|
|
</svg>
|
|
|
|
{/* Text as a separate element to position it at the top */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '40%',
|
|
left: '45%',
|
|
transform: 'translate(-50%, -50%) rotate(-10deg)',
|
|
color: 'white',
|
|
fontWeight: '900',
|
|
fontSize: '16px',
|
|
textShadow: '0px 1px 2px rgba(0,0,0,0.5)',
|
|
zIndex: 1000
|
|
}}
|
|
>
|
|
{this.props.t ? this.props.t('product.new') : 'NEU'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<Card
|
|
sx={{
|
|
width: {
|
|
xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`,
|
|
sm: `${PRODUCT_CARD_WIDTH_SM_PX}px`,
|
|
},
|
|
minWidth: {
|
|
xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`,
|
|
sm: `${PRODUCT_CARD_WIDTH_SM_PX}px`,
|
|
},
|
|
maxWidth: {
|
|
xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`,
|
|
sm: `${PRODUCT_CARD_WIDTH_SM_PX}px`,
|
|
},
|
|
height: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
borderRadius: { xs: '8px', sm: '8px' },
|
|
border: { xs: '1px solid', sm: 'inherit' },
|
|
borderColor: { xs: 'divider', sm: 'inherit' },
|
|
boxShadow: { xs: '0 1px 4px rgba(0,0,0,0.08)', sm: 'inherit' },
|
|
mx: { xs: 'auto', sm: 'auto' },
|
|
'&:hover': {
|
|
transform: { xs: 'none', sm: 'translateY(-5px)' },
|
|
boxShadow: {
|
|
xs: '0 1px 4px rgba(0,0,0,0.08)',
|
|
sm: '0px 10px 20px rgba(0,0,0,0.1)',
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
{showThcBadge && (
|
|
<div aria-label={`THC Anteil: ${thc}%`}
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
backgroundColor: thcBadgeColor,
|
|
color: thc > 25 && thc <= 30 ? '#000000' : '#ffffff',
|
|
fontWeight: 'bold',
|
|
padding: '2px 0',
|
|
width: '80px',
|
|
textAlign: 'center',
|
|
zIndex: 999,
|
|
fontSize: '9px',
|
|
boxShadow: '0px 2px 4px rgba(0,0,0,0.2)',
|
|
transform: 'rotate(-45deg) translateX(-40px) translateY(15px)',
|
|
transformOrigin: 'top left'
|
|
}}
|
|
>
|
|
THC {thc}%
|
|
</div>
|
|
)}
|
|
|
|
{showFloweringWeeksBadge && (
|
|
<div aria-label={`Flowering Weeks: ${floweringWeeks}`}
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
backgroundColor: floweringWeeksBadgeColor,
|
|
color: floweringWeeks > 8 && floweringWeeks <= 12 ? '#000000' : '#ffffff',
|
|
fontWeight: 'bold',
|
|
padding: '1px 0',
|
|
width: '100px',
|
|
textAlign: 'center',
|
|
zIndex: 999,
|
|
fontSize: '9px',
|
|
boxShadow: '0px 2px 4px rgba(0,0,0,0.2)',
|
|
transform: 'rotate(-45deg) translateX(-50px) translateY(32px)',
|
|
transformOrigin: 'top left'
|
|
}}
|
|
>
|
|
{floweringWeeks} {this.props.t ? this.props.t('product.weeks') : 'Wochen'}
|
|
</div>
|
|
)}
|
|
|
|
<Box
|
|
onClick={this.handleProductClick}
|
|
sx={{
|
|
flexGrow: 1,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'stretch',
|
|
textDecoration: 'none',
|
|
color: 'inherit',
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
<Box
|
|
onMouseMove={this.handleMouseMove}
|
|
onMouseLeave={this.handleMouseLeave}
|
|
sx={{
|
|
position: 'relative',
|
|
height: { xs: '240px', sm: '180px' },
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: '#ffffff',
|
|
borderTopLeftRadius: '8px',
|
|
borderTopRightRadius: '8px'
|
|
}}>
|
|
{this.state.loading ? (
|
|
<CircularProgress sx={{ color: '#90ffc0' }} />
|
|
) : this.state.images && this.state.images.length > 0 && this.state.images.some(img => img !== null) ? (
|
|
this.state.images.map((imgSrc, index) => {
|
|
if (!imgSrc) return null;
|
|
return (
|
|
<CardMedia
|
|
key={index}
|
|
component="img"
|
|
height={window.innerWidth < PRODUCT_CARD_MOBILE_MAX_WIDTH_PX ? "240" : "180"}
|
|
image={imgSrc}
|
|
alt={name}
|
|
fetchPriority={this.props.priority === 'high' && index === 0 ? 'high' : 'auto'}
|
|
loading={this.props.priority === 'high' && index === 0 ? 'eager' : 'lazy'}
|
|
onError={(e) => {
|
|
// Ensure alt text is always present even on error
|
|
if (!e.target.alt) {
|
|
e.target.alt = name || 'Produktbild';
|
|
}
|
|
}}
|
|
sx={{
|
|
objectFit: 'contain',
|
|
borderTopLeftRadius: '8px',
|
|
borderTopRightRadius: '8px',
|
|
width: '100%',
|
|
height: '100%',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
opacity: this.state.currentImageIndex === index ? 1 : 0,
|
|
transition: this.state.isHovering ? 'opacity 0.2s ease-in-out' : 'opacity 1s ease-in-out'
|
|
}}
|
|
/>
|
|
);
|
|
})
|
|
) : (
|
|
<CardMedia
|
|
component="img"
|
|
height={window.innerWidth < PRODUCT_CARD_MOBILE_MAX_WIDTH_PX ? "240" : "180"}
|
|
image="/assets/images/nopicture.jpg"
|
|
alt={name}
|
|
fetchPriority={this.props.priority === 'high' ? 'high' : 'auto'}
|
|
loading={this.props.priority === 'high' ? 'eager' : 'lazy'}
|
|
onError={(e) => {
|
|
// Ensure alt text is always present even on error
|
|
if (!e.target.alt) {
|
|
e.target.alt = name || 'Produktbild';
|
|
}
|
|
}}
|
|
sx={{
|
|
objectFit: 'contain',
|
|
borderTopLeftRadius: '8px',
|
|
borderTopRightRadius: '8px',
|
|
width: '100%',
|
|
height: '100%',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
|
|
<CardContent sx={{
|
|
flexGrow: 1,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
'&.MuiCardContent-root:last-child': {
|
|
paddingBottom: 0
|
|
}
|
|
}}>
|
|
<Typography
|
|
gutterBottom
|
|
variant="h6"
|
|
component="h2"
|
|
sx={{
|
|
fontWeight: 600,
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: 2,
|
|
WebkitBoxOrient: 'vertical',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
minHeight: '3.4em'
|
|
}}
|
|
>
|
|
{name}
|
|
</Typography>
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
|
<Typography variant="body2" color="text.secondary" style={{ minHeight: '1.5em' }}>
|
|
{manufacturer || ''}
|
|
</Typography>
|
|
</Box>
|
|
|
|
<div style={{ padding: '0px', margin: '0px' }}>
|
|
<Typography
|
|
variant="h6"
|
|
color="primary"
|
|
sx={{
|
|
fontWeight: 'bold',
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
position: 'relative'
|
|
}}
|
|
>
|
|
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
|
{this.props.rebate && this.props.rebate > 0 && (
|
|
<span
|
|
style={{
|
|
position: 'absolute',
|
|
top: -8,
|
|
left: -8,
|
|
fontWeight: 'bold',
|
|
color: 'red',
|
|
textDecoration: 'line-through',
|
|
opacity: 0.4,
|
|
zIndex: 1,
|
|
pointerEvents: 'none',
|
|
fontSize: 'inherit'
|
|
}}
|
|
>
|
|
{(() => {
|
|
const rebatePct = this.props.rebate / 100;
|
|
const originalPrice = Math.round((price / (1 - rebatePct)) * 10) / 10;
|
|
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: currency || 'EUR' }).format(originalPrice);
|
|
})()}
|
|
</span>
|
|
)}
|
|
<span style={{ position: 'relative', zIndex: 2 }}>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: currency || 'EUR' }).format(price)}</span>
|
|
</Box>
|
|
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>({this.props.t ? this.props.t('product.inclVatFooter', { vat }) : `incl. ${vat}% USt.,*`})</small>
|
|
</Typography>
|
|
</div>
|
|
<div style={{ minHeight: '1.5em' }}>
|
|
{cGrundEinheit && fGrundPreis && fGrundPreis != price && (<Typography variant="body2" color="text.secondary" sx={{ m: 0, p: 0 }}>
|
|
({new Intl.NumberFormat('de-DE', { style: 'currency', currency: currency || 'EUR' }).format(fGrundPreis)}/{cGrundEinheit})
|
|
</Typography>)}
|
|
</div>
|
|
{/*incoming*/}
|
|
</CardContent>
|
|
</Box>
|
|
|
|
<Box sx={{ p: 2, pt: 0, display: 'flex', alignItems: 'center' }}>
|
|
<IconButton
|
|
component={Link}
|
|
to={`/Artikel/${seoName}`}
|
|
size="small"
|
|
aria-label="Produktdetails anzeigen"
|
|
sx={{ mr: 1, color: 'text.secondary' }}
|
|
>
|
|
<ZoomInIcon />
|
|
</IconButton>
|
|
<AddToCartButton cartButton={true} availableSupplier={availableSupplier} komponenten={komponenten} cGrundEinheit={cGrundEinheit} fGrundPreis={fGrundPreis} available={available} incoming={incoming} seoName={seoName} pictureList={this.props.pictureList} id={id} price={price} vat={vat} weight={weight} name={name} versandklasse={versandklasse} />
|
|
</Box>
|
|
</Card>
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
// Wrapper component to provide navigate hook
|
|
const ProductWithNavigation = (props) => {
|
|
const navigate = useNavigate();
|
|
return <Product {...props} navigate={navigate} />;
|
|
};
|
|
|
|
export default withI18n()(ProductWithNavigation);
|