diff --git a/public/assets/images/404.png b/public/assets/images/404.png index 1d6c8bd..11fd74d 100644 Binary files a/public/assets/images/404.png and b/public/assets/images/404.png differ diff --git a/src/components/AddToCartButton.js b/src/components/AddToCartButton.js index 05e0457..a43cb27 100644 --- a/src/components/AddToCartButton.js +++ b/src/components/AddToCartButton.js @@ -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 { diff --git a/src/components/CartItem.js b/src/components/CartItem.js index bd18945..7cf602e 100644 --- a/src/components/CartItem.js +++ b/src/components/CartItem.js @@ -150,7 +150,7 @@ class CartItem extends Component { item.available == 1 ? "Lieferzeit: 2-3 Tage" : item.availableSupplier == 1 ? "Lieferzeit: 7-9 Tage" : ""} - + diff --git a/src/components/Product.js b/src/components/Product.js index 1b8ce82..2cee78f 100644 --- a/src/components/Product.js +++ b/src/components/Product.js @@ -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 { > - + diff --git a/src/components/ProductDetailPage.js b/src/components/ProductDetailPage.js index 5073701..cc0cd07 100644 --- a/src/components/ProductDetailPage.js +++ b/src/components/ProductDetailPage.js @@ -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 { )} + + {/* 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) && ( + + + + Sie sparen: {new Intl.NumberFormat("de-DE", { + style: "currency", + currency: "EUR", + }).format(totalKomponentenPrice - product.price)} + + + Günstiger als Einzelkauf + + + + )} + @@ -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 { )} + + {product.komponenten && product.komponenten.split(",").length > 0 && ( + + Bestehend aus: + + + {(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 ( + + + + {/* Empty placeholder for image */} + + + + {index + 1}. Lädt... + + + {komponent.count}x + + + + + - + + + ); + } + + const itemPrice = komponentData.price * parseInt(komponent.count); + const formattedPrice = new Intl.NumberFormat("de-DE", { + style: "currency", + currency: "EUR", + }).format(itemPrice); + + return ( + + + + {komponentenImages[komponent.id] ? ( + + ) : ( + + )} + + + + {index + 1}. {cleanProductName(komponentData.name)} + + + {komponent.count}x à {new Intl.NumberFormat("de-DE", { + style: "currency", + currency: "EUR", + }).format(komponentData.price)} + + + + + {formattedPrice} + + + ); + })} + + {/* 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) && ( + + + + Einzelpreis gesamt: + + + {new Intl.NumberFormat("de-DE", { + style: "currency", + currency: "EUR", + }).format(totalKomponentenPrice)} + + + + + Set-Preis: + + + {new Intl.NumberFormat("de-DE", { + style: "currency", + currency: "EUR", + }).format(product.price)} + + + {totalSavings > 0 && ( + + + Ihre Ersparnis: + + + {new Intl.NumberFormat("de-DE", { + style: "currency", + currency: "EUR", + }).format(totalSavings)} + + + )} + + )} + + ) : ( + // Loading state + + {this.state.komponenten.map((komponent, index) => { + // For loading state, we don't know if pricing will be shown, so show all borders + return ( + + + + {/* Empty placeholder for image */} + + + + {index + 1}. Lädt Komponent-Details... + + + {komponent.count}x + + + + + - + + + ); + })} + + )} + + + )} ); } diff --git a/src/components/ProductList.js b/src/components/ProductList.js index bc4349e..678ab1b 100644 --- a/src/components/ProductList.js +++ b/src/components/ProductList.js @@ -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} /> ))}