Compare commits
2 Commits
b33ece2875
...
9000b28ce5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9000b28ce5 | ||
|
|
8f2253f155 |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 85 KiB |
@@ -252,7 +252,7 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
|
||||
|
||||
{/* Grow Tent Configurator */}
|
||||
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
|
||||
<Route path="/Konfigurator" element={<GrowTentKonfigurator socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Separate pages that are truly different */}
|
||||
<Route path="/presseverleih" element={<PresseverleihPage />} />
|
||||
|
||||
@@ -58,6 +58,7 @@ class AddToCartButton extends Component {
|
||||
vat: this.props.vat,
|
||||
versandklasse: this.props.versandklasse,
|
||||
availableSupplier: this.props.availableSupplier,
|
||||
komponenten: this.props.komponenten,
|
||||
available: this.props.available
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -150,7 +150,7 @@ class CartItem extends Component {
|
||||
item.available == 1 ? "Lieferzeit: 2-3 Tage" :
|
||||
item.availableSupplier == 1 ? "Lieferzeit: 7-9 Tage" : ""}
|
||||
</Typography>
|
||||
<AddToCartButton available={1} id={this.props.id} availableSupplier={item.availableSupplier} price={item.price} seoName={item.seoName} name={item.name} weight={item.weight} vat={item.vat} versandklasse={item.versandklasse}/>
|
||||
<AddToCartButton available={1} id={this.props.id} komponenten={item.komponenten} availableSupplier={item.availableSupplier} price={item.price} seoName={item.seoName} name={item.name} weight={item.weight} vat={item.vat} versandklasse={item.versandklasse}/>
|
||||
</Box>
|
||||
</Box>
|
||||
</ListItem>
|
||||
|
||||
@@ -41,18 +41,17 @@ const MainPageLayout = () => {
|
||||
return { leftNav: null, rightNav: null };
|
||||
};
|
||||
|
||||
// Define all titles for layered rendering
|
||||
const allTitles = {
|
||||
home: "ine annabis eeds & uttings",
|
||||
aktionen: "Aktionen",
|
||||
filiale: "Filiale"
|
||||
aktionen: "tuelle ktionen & gebote",
|
||||
filiale: "nsere iliale in resden"
|
||||
};
|
||||
|
||||
// Define all content boxes for layered rendering
|
||||
const allContentBoxes = {
|
||||
home: [
|
||||
{
|
||||
title: "Samen",
|
||||
title: "Seeds",
|
||||
image: "/assets/images/seeds.jpg",
|
||||
bgcolor: "#e1f0d3",
|
||||
link: "/Kategorie/Seeds"
|
||||
|
||||
@@ -69,7 +69,7 @@ class Product extends Component {
|
||||
const {
|
||||
id, name, price, available, manufacturer, seoName,
|
||||
currency, vat, cGrundEinheit, fGrundPreis, thc,
|
||||
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier
|
||||
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier, komponenten
|
||||
} = this.props;
|
||||
|
||||
const isNew = neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
||||
@@ -358,7 +358,7 @@ class Product extends Component {
|
||||
>
|
||||
<ZoomInIcon />
|
||||
</IconButton>
|
||||
<AddToCartButton cartButton={true} availableSupplier={availableSupplier} 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}/>
|
||||
<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>
|
||||
|
||||
@@ -29,6 +29,12 @@ class ProductDetailPage extends Component {
|
||||
attributes: [],
|
||||
isSteckling: false,
|
||||
imageDialogOpen: false,
|
||||
komponenten: [],
|
||||
komponentenLoaded: false,
|
||||
komponentenData: {}, // Store individual komponent data with loading states
|
||||
komponentenImages: {}, // Store tiny pictures for komponenten
|
||||
totalKomponentenPrice: 0,
|
||||
totalSavings: 0
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
@@ -39,6 +45,12 @@ class ProductDetailPage extends Component {
|
||||
attributes: [],
|
||||
isSteckling: false,
|
||||
imageDialogOpen: false,
|
||||
komponenten: [],
|
||||
komponentenLoaded: false,
|
||||
komponentenData: {}, // Store individual komponent data with loading states
|
||||
komponentenImages: {}, // Store tiny pictures for komponenten
|
||||
totalKomponentenPrice: 0,
|
||||
totalSavings: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -64,6 +76,248 @@ class ProductDetailPage extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
loadKomponentImage = (komponentId, pictureList) => {
|
||||
// Initialize cache if it doesn't exist
|
||||
if (!window.smallPicCache) {
|
||||
window.smallPicCache = {};
|
||||
}
|
||||
|
||||
// Skip if no pictureList
|
||||
if (!pictureList || pictureList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first image ID from pictureList
|
||||
const bildId = pictureList.split(',')[0];
|
||||
|
||||
// Check if already cached
|
||||
if (window.smallPicCache[bildId]) {
|
||||
this.setState(prevState => ({
|
||||
komponentenImages: {
|
||||
...prevState.komponentenImages,
|
||||
[komponentId]: window.smallPicCache[bildId]
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if socketB is available
|
||||
if (!this.props.socketB || !this.props.socketB.connected) {
|
||||
console.log("SocketB not connected yet, skipping image load for komponent:", komponentId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch image from server
|
||||
this.props.socketB.emit('getPic', { bildId, size: 'small' }, (res) => {
|
||||
if (res.success) {
|
||||
// Cache the image
|
||||
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
||||
|
||||
// Update state
|
||||
this.setState(prevState => ({
|
||||
komponentenImages: {
|
||||
...prevState.komponentenImages,
|
||||
[komponentId]: window.smallPicCache[bildId]
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
console.log('Error loading komponent image:', res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadKomponent = (id, count) => {
|
||||
// Initialize cache if it doesn't exist
|
||||
if (!window.productDetailCache) {
|
||||
window.productDetailCache = {};
|
||||
}
|
||||
|
||||
// Check if this komponent is already cached
|
||||
if (window.productDetailCache[id]) {
|
||||
const cachedProduct = window.productDetailCache[id];
|
||||
|
||||
// Load komponent image if available
|
||||
if (cachedProduct.pictureList) {
|
||||
this.loadKomponentImage(id, cachedProduct.pictureList);
|
||||
}
|
||||
|
||||
// Update state with cached data
|
||||
this.setState(prevState => {
|
||||
const newKomponentenData = {
|
||||
...prevState.komponentenData,
|
||||
[id]: {
|
||||
...cachedProduct,
|
||||
count: parseInt(count),
|
||||
loaded: true
|
||||
}
|
||||
};
|
||||
|
||||
// Check if all remaining komponenten are loaded
|
||||
const allLoaded = prevState.komponenten.every(k =>
|
||||
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||
);
|
||||
|
||||
// Calculate totals if all loaded
|
||||
let totalKomponentenPrice = 0;
|
||||
let totalSavings = 0;
|
||||
|
||||
if (allLoaded) {
|
||||
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
|
||||
const komponentData = newKomponentenData[k.id];
|
||||
if (komponentData && komponentData.loaded) {
|
||||
return sum + (komponentData.price * parseInt(k.count));
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
// Calculate savings (difference between buying individually vs as set)
|
||||
const setPrice = prevState.product ? prevState.product.price : 0;
|
||||
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||
}
|
||||
|
||||
console.log("Cached komponent loaded:", id, "data:", newKomponentenData[id]);
|
||||
console.log("All loaded (cached):", allLoaded);
|
||||
|
||||
return {
|
||||
komponentenData: newKomponentenData,
|
||||
komponentenLoaded: allLoaded,
|
||||
totalKomponentenPrice,
|
||||
totalSavings
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If not cached, fetch from server (similar to loadProductData)
|
||||
if (!this.props.socket || !this.props.socket.connected) {
|
||||
console.log("Socket not connected yet, waiting for connection to load komponent data");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark this komponent as loading
|
||||
this.setState(prevState => ({
|
||||
komponentenData: {
|
||||
...prevState.komponentenData,
|
||||
[id]: {
|
||||
...prevState.komponentenData[id],
|
||||
loading: true,
|
||||
loaded: false,
|
||||
count: parseInt(count)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.props.socket.emit(
|
||||
"getProductView",
|
||||
{ articleId: id },
|
||||
(res) => {
|
||||
if (res.success) {
|
||||
// Cache the successful response
|
||||
window.productDetailCache[id] = res.product;
|
||||
|
||||
// Load komponent image if available
|
||||
if (res.product.pictureList) {
|
||||
this.loadKomponentImage(id, res.product.pictureList);
|
||||
}
|
||||
|
||||
// Update state with loaded data
|
||||
this.setState(prevState => {
|
||||
const newKomponentenData = {
|
||||
...prevState.komponentenData,
|
||||
[id]: {
|
||||
...res.product,
|
||||
count: parseInt(count),
|
||||
loading: false,
|
||||
loaded: true
|
||||
}
|
||||
};
|
||||
|
||||
// Check if all remaining komponenten are loaded
|
||||
const allLoaded = prevState.komponenten.every(k =>
|
||||
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||
);
|
||||
|
||||
// Calculate totals if all loaded
|
||||
let totalKomponentenPrice = 0;
|
||||
let totalSavings = 0;
|
||||
|
||||
if (allLoaded) {
|
||||
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
|
||||
const komponentData = newKomponentenData[k.id];
|
||||
if (komponentData && komponentData.loaded) {
|
||||
return sum + (komponentData.price * parseInt(k.count));
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
// Calculate savings (difference between buying individually vs as set)
|
||||
const setPrice = prevState.product ? prevState.product.price : 0;
|
||||
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||
}
|
||||
|
||||
console.log("Updated komponentenData for", id, ":", newKomponentenData[id]);
|
||||
console.log("All loaded:", allLoaded);
|
||||
|
||||
return {
|
||||
komponentenData: newKomponentenData,
|
||||
komponentenLoaded: allLoaded,
|
||||
totalKomponentenPrice,
|
||||
totalSavings
|
||||
};
|
||||
});
|
||||
|
||||
console.log("getProductView (komponent)", res);
|
||||
} else {
|
||||
console.error("Error loading komponent:", res.error || "Unknown error", res);
|
||||
|
||||
// Remove failed komponent from the list and check if all remaining are loaded
|
||||
this.setState(prevState => {
|
||||
const newKomponenten = prevState.komponenten.filter(k => k.id !== id);
|
||||
const newKomponentenData = { ...prevState.komponentenData };
|
||||
|
||||
// Remove failed komponent from data
|
||||
delete newKomponentenData[id];
|
||||
|
||||
// Check if all remaining komponenten are loaded
|
||||
const allLoaded = newKomponenten.length === 0 || newKomponenten.every(k =>
|
||||
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||
);
|
||||
|
||||
// Calculate totals if all loaded
|
||||
let totalKomponentenPrice = 0;
|
||||
let totalSavings = 0;
|
||||
|
||||
if (allLoaded && newKomponenten.length > 0) {
|
||||
totalKomponentenPrice = newKomponenten.reduce((sum, k) => {
|
||||
const komponentData = newKomponentenData[k.id];
|
||||
if (komponentData && komponentData.loaded) {
|
||||
return sum + (komponentData.price * parseInt(k.count));
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
// Calculate savings (difference between buying individually vs as set)
|
||||
const setPrice = this.state.product ? this.state.product.price : 0;
|
||||
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||
}
|
||||
|
||||
console.log("Removed failed komponent:", id, "remaining:", newKomponenten.length);
|
||||
console.log("All loaded after removal:", allLoaded);
|
||||
|
||||
return {
|
||||
komponenten: newKomponenten,
|
||||
komponentenData: newKomponentenData,
|
||||
komponentenLoaded: allLoaded,
|
||||
totalKomponentenPrice,
|
||||
totalSavings
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadProductData = () => {
|
||||
if (!this.props.socket || !this.props.socket.connected) {
|
||||
// Socket not connected yet, but don't show error immediately on first load
|
||||
@@ -78,12 +332,37 @@ class ProductDetailPage extends Component {
|
||||
(res) => {
|
||||
if (res.success) {
|
||||
res.product.seoName = this.props.seoName;
|
||||
|
||||
// Initialize cache if it doesn't exist
|
||||
if (!window.productDetailCache) {
|
||||
window.productDetailCache = {};
|
||||
}
|
||||
|
||||
// Cache the product data
|
||||
window.productDetailCache[this.props.seoName] = res.product;
|
||||
|
||||
const komponenten = [];
|
||||
if(res.product.komponenten) {
|
||||
for(const komponent of res.product.komponenten.split(",")) {
|
||||
// Handle both "x" and "×" as separators
|
||||
const [id, count] = komponent.split(/[x×]/);
|
||||
komponenten.push({id: id.trim(), count: count.trim()});
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
product: res.product,
|
||||
loading: false,
|
||||
error: null,
|
||||
imageDialogOpen: false,
|
||||
attributes: res.attributes
|
||||
attributes: res.attributes,
|
||||
komponenten: komponenten,
|
||||
komponentenLoaded: komponenten.length === 0 // If no komponenten, mark as loaded
|
||||
}, () => {
|
||||
if(komponenten.length > 0) {
|
||||
for(const komponent of komponenten) {
|
||||
this.loadKomponent(komponent.id, komponent.count);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log("getProductView", res);
|
||||
|
||||
@@ -180,7 +459,7 @@ class ProductDetailPage extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { product, loading, error, attributeImages, isSteckling, attributes } =
|
||||
const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } =
|
||||
this.state;
|
||||
|
||||
if (loading) {
|
||||
@@ -465,6 +744,37 @@ class ProductDetailPage extends Component {
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Savings comparison - positioned between price and cart button */}
|
||||
{product.komponenten && komponentenLoaded && totalKomponentenPrice > product.price &&
|
||||
(totalKomponentenPrice - product.price >= 2 &&
|
||||
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minWidth: { xs: "100%", sm: "200px" }
|
||||
}}>
|
||||
<Box sx={{ p: 2, borderRadius: 1, backgroundColor: "#e8f5e8", textAlign: "center" }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
color: "success.main"
|
||||
}}
|
||||
>
|
||||
Sie sparen: {new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(totalKomponentenPrice - product.price)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Günstiger als Einzelkauf
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -491,6 +801,7 @@ class ProductDetailPage extends Component {
|
||||
vat={product.vat}
|
||||
weight={product.weight}
|
||||
availableSupplier={product.availableSupplier}
|
||||
komponenten={product.komponenten}
|
||||
name={cleanProductName(product.name) + " Stecklingsvorbestellung 1 Stück"}
|
||||
versandklasse={"nur Abholung"}
|
||||
/>
|
||||
@@ -520,6 +831,7 @@ class ProductDetailPage extends Component {
|
||||
available={product.available}
|
||||
id={product.id}
|
||||
availableSupplier={product.availableSupplier}
|
||||
komponenten={product.komponenten}
|
||||
cGrundEinheit={product.cGrundEinheit}
|
||||
fGrundPreis={product.fGrundPreis}
|
||||
price={product.price}
|
||||
@@ -572,6 +884,206 @@ class ProductDetailPage extends Component {
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{product.komponenten && product.komponenten.split(",").length > 0 && (
|
||||
<Box sx={{ mt: 4, p: 4, background: "#fff", borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
|
||||
<Typography variant="h4" gutterBottom>Bestehend aus:</Typography>
|
||||
<Box sx={{ maxWidth: 800, mx: "auto" }}>
|
||||
|
||||
{(console.log("komponentenLoaded:", komponentenLoaded), komponentenLoaded) ? (
|
||||
<>
|
||||
{console.log("Rendering loaded komponenten:", this.state.komponenten.length, "komponentenData:", Object.keys(komponentenData).length)}
|
||||
{this.state.komponenten.map((komponent, index) => {
|
||||
const komponentData = komponentenData[komponent.id];
|
||||
console.log(`Rendering komponent ${komponent.id}:`, komponentData);
|
||||
|
||||
// Don't show border on last item (pricing section has its own top border)
|
||||
const isLastItem = index === this.state.komponenten.length - 1;
|
||||
const showBorder = !isLastItem;
|
||||
|
||||
if (!komponentData || !komponentData.loaded) {
|
||||
return (
|
||||
<Box key={komponent.id} sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
borderBottom: showBorder ? "1px solid #eee" : "none",
|
||||
minHeight: "70px" // Consistent height to prevent layout shifts
|
||||
}}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||||
{/* Empty placeholder for image */}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body1">
|
||||
{index + 1}. Lädt...
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{komponent.count}x
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
-
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const itemPrice = komponentData.price * parseInt(komponent.count);
|
||||
const formattedPrice = new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(itemPrice);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={komponent.id}
|
||||
component={Link}
|
||||
to={`/Artikel/${komponentData.seoName}`}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
borderBottom: showBorder ? "1px solid #eee" : "none",
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
minHeight: "70px", // Consistent height to prevent layout shifts
|
||||
"&:hover": {
|
||||
backgroundColor: "#f5f5f5"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<Box sx={{ width: 50, height: 50, flexShrink: 0 }}>
|
||||
{komponentenImages[komponent.id] ? (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="50"
|
||||
image={komponentenImages[komponent.id]}
|
||||
alt={komponentData.name}
|
||||
sx={{
|
||||
objectFit: "contain",
|
||||
borderRadius: 1,
|
||||
border: "1px solid #e0e0e0"
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="50"
|
||||
image="/assets/images/nopicture.jpg"
|
||||
alt={komponentData.name}
|
||||
sx={{
|
||||
objectFit: "contain",
|
||||
borderRadius: 1,
|
||||
border: "1px solid #e0e0e0"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
{index + 1}. {cleanProductName(komponentData.name)}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{komponent.count}x à {new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(komponentData.price)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
{formattedPrice}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Total price and savings display - only show when prices differ meaningfully */}
|
||||
{totalKomponentenPrice > product.price &&
|
||||
(totalKomponentenPrice - product.price >= 2 &&
|
||||
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
|
||||
<Box sx={{ mt: 3, pt: 2, borderTop: "2px solid #eee" }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
|
||||
<Typography variant="h6">
|
||||
Einzelpreis gesamt:
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ textDecoration: "line-through", color: "text.secondary" }}>
|
||||
{new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(totalKomponentenPrice)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
|
||||
<Typography variant="h6">
|
||||
Set-Preis:
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary" sx={{ fontWeight: "bold" }}>
|
||||
{new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(product.price)}
|
||||
</Typography>
|
||||
</Box>
|
||||
{totalSavings > 0 && (
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mt: 2, p: 2, backgroundColor: "#e8f5e8", borderRadius: 1 }}>
|
||||
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
|
||||
Ihre Ersparnis:
|
||||
</Typography>
|
||||
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
|
||||
{new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(totalSavings)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// Loading state
|
||||
<Box>
|
||||
{this.state.komponenten.map((komponent, index) => {
|
||||
// For loading state, we don't know if pricing will be shown, so show all borders
|
||||
return (
|
||||
<Box key={komponent.id} sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
borderBottom: "1px solid #eee",
|
||||
minHeight: "70px" // Consistent height to prevent layout shifts
|
||||
}}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||||
{/* Empty placeholder for image */}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body1">
|
||||
{index + 1}. Lädt Komponent-Details...
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{komponent.count}x
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
-
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,12 +141,12 @@ class ProductList extends Component {
|
||||
onChange={this.handlePageChange}
|
||||
color="primary"
|
||||
size={"large"}
|
||||
siblingCount={window.innerWidth < 600 ? 0 : 1}
|
||||
boundaryCount={window.innerWidth < 600 ? 1 : 1}
|
||||
hideNextButton={false}
|
||||
hidePrevButton={false}
|
||||
showFirstButton={window.innerWidth >= 600}
|
||||
showLastButton={window.innerWidth >= 600}
|
||||
siblingCount={1}
|
||||
boundaryCount={1}
|
||||
hideNextButton={true}
|
||||
hidePrevButton={true}
|
||||
showFirstButton={false}
|
||||
showLastButton={false}
|
||||
sx={{
|
||||
'& .MuiPagination-ul': {
|
||||
flexWrap: 'nowrap',
|
||||
@@ -474,6 +474,7 @@ class ProductList extends Component {
|
||||
socketB={this.props.socketB}
|
||||
pictureList={product.pictureList}
|
||||
availableSupplier={product.availableSupplier}
|
||||
komponenten={product.komponenten}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
@@ -17,6 +17,51 @@ import {
|
||||
import { TentShapeSelector, ProductSelector, ExtrasSelector } from '../components/configurator/index.js';
|
||||
import { tentShapes, tentSizes, lightTypes, ventilationTypes, extras } from '../data/configuratorData.js';
|
||||
|
||||
function setCachedCategoryData(categoryId, data) {
|
||||
if (!window.productCache) {
|
||||
window.productCache = {};
|
||||
}
|
||||
if (!window.productDetailCache) {
|
||||
window.productDetailCache = {};
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheKey = `categoryProducts_${categoryId}`;
|
||||
if(data.products) for(const product of data.products) {
|
||||
window.productDetailCache[product.id] = product;
|
||||
}
|
||||
window.productCache[cacheKey] = {
|
||||
...data,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error writing to cache:', err);
|
||||
}
|
||||
}
|
||||
function getCachedCategoryData(categoryId) {
|
||||
if (!window.productCache) {
|
||||
window.productCache = {};
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheKey = `categoryProducts_${categoryId}`;
|
||||
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;
|
||||
}
|
||||
|
||||
class GrowTentKonfigurator extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -40,6 +85,8 @@ class GrowTentKonfigurator extends Component {
|
||||
this.handleExtraToggle = this.handleExtraToggle.bind(this);
|
||||
this.calculateTotalPrice = this.calculateTotalPrice.bind(this);
|
||||
this.saveStateToWindow = this.saveStateToWindow.bind(this);
|
||||
|
||||
|
||||
}
|
||||
|
||||
saveStateToWindow() {
|
||||
@@ -57,7 +104,10 @@ class GrowTentKonfigurator extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
// @note Calculate initial total price with preselected products
|
||||
this.calculateTotalPrice();
|
||||
//this.calculateTotalPrice();
|
||||
this.fetchCategoryData("Zelte");
|
||||
this.fetchCategoryData("Lampen");
|
||||
this.fetchCategoryData("Abluft-sets");
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@@ -89,6 +139,31 @@ class GrowTentKonfigurator extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
fetchCategoryData(categoryId) {
|
||||
const cachedData = getCachedCategoryData(categoryId);
|
||||
if (cachedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.props.socket || !this.props.socket.connected) {
|
||||
console.log("Socket not connected yet, waiting for connection to fetch category data");
|
||||
return;
|
||||
}
|
||||
console.log(`productList:${categoryId}`);
|
||||
this.props.socket.off(`productList:${categoryId}`);
|
||||
|
||||
this.props.socket.on(`productList:${categoryId}`,(response) => {
|
||||
console.log("getCategoryProducts full response", response);
|
||||
setCachedCategoryData(categoryId, response);
|
||||
});
|
||||
|
||||
this.props.socket.emit("getCategoryProducts", { categoryId: categoryId },
|
||||
(response) => {
|
||||
console.log("getCategoryProducts stub response", response);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleTentShapeSelect(shapeId) {
|
||||
this.setState({ selectedTentShape: shapeId });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user