- Wrapped AppContent with ProductContextProvider and CategoryContextProvider to manage product and category states. - Added TitleUpdater component for dynamic title management. - Enhanced Content and ProductDetailPage components to utilize the new context for setting and clearing current product and category states. - Updated ProductDetailWithSocket to pass setCurrentProduct function from context.
2011 lines
75 KiB
JavaScript
2011 lines
75 KiB
JavaScript
import React, { Component } from "react";
|
||
import { Box, Container, Typography, CardMedia, Stack, Chip, Button, Collapse, Popper, ClickAwayListener, MenuList, MenuItem, ListItemIcon, ListItemText, Snackbar, Alert } from "@mui/material";
|
||
import ShareIcon from "@mui/icons-material/Share";
|
||
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
|
||
import FacebookIcon from "@mui/icons-material/Facebook";
|
||
import TelegramIcon from "@mui/icons-material/Telegram";
|
||
import EmailIcon from "@mui/icons-material/Email";
|
||
import LinkIcon from "@mui/icons-material/Link";
|
||
import CodeIcon from "@mui/icons-material/Code";
|
||
import { Link } from "react-router-dom";
|
||
import parse from "html-react-parser";
|
||
import sanitizeHtml from "sanitize-html";
|
||
import AddToCartButton from "./AddToCartButton.js";
|
||
import ProductImage from "./ProductImage.js";
|
||
import Product from "./Product.js";
|
||
import { withI18n } from "../i18n/withTranslation.js";
|
||
import ArticleQuestionForm from "./ArticleQuestionForm.js";
|
||
import ArticleRatingForm from "./ArticleRatingForm.js";
|
||
import ArticleAvailabilityForm from "./ArticleAvailabilityForm.js";
|
||
|
||
// Utility function to clean product names by removing trailing number in parentheses
|
||
const cleanProductName = (name) => {
|
||
if (!name) return "";
|
||
// Remove patterns like " (1)", " (3)", " (10)" at the end of the string
|
||
return name.replace(/\s*\(\d+\)\s*$/, "").trim();
|
||
};
|
||
|
||
// Product detail page with image loading
|
||
class ProductDetailPage extends Component {
|
||
constructor(props) {
|
||
super(props);
|
||
|
||
// First try to find cached data by seoName (complete data)
|
||
let cachedData = null;
|
||
let partialProduct = null;
|
||
let isUpgrading = false;
|
||
|
||
// Get current language for cache key
|
||
const currentLanguage = this.props.i18n?.language || 'de';
|
||
const cacheKey = `product_${this.props.seoName}_${currentLanguage}`;
|
||
|
||
if (window.productDetailCache && window.productDetailCache[cacheKey]) {
|
||
cachedData = window.productDetailCache[cacheKey];
|
||
} else if (window.productDetailCache) {
|
||
// If not found by seoName, search for partial data by checking all cached products
|
||
// Look for a product where the seoName matches this.props.seoName
|
||
for (const key in window.productDetailCache) {
|
||
const cached = window.productDetailCache[key];
|
||
if (cached && cached.product && cached.product.seoName === this.props.seoName) {
|
||
partialProduct = cached.product;
|
||
isUpgrading = true;
|
||
break;
|
||
}
|
||
// Also check if cached is a product object directly (from category cache)
|
||
if (cached && typeof cached === 'object' && cached.seoName === this.props.seoName) {
|
||
partialProduct = cached;
|
||
isUpgrading = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (cachedData) {
|
||
// Complete cached data found
|
||
|
||
// Clean up prerender fallback since we have cached data
|
||
if (typeof window !== "undefined" && window.__PRERENDER_FALLBACK__) {
|
||
delete window.__PRERENDER_FALLBACK__;
|
||
console.log("ProductDetailPage: Cleaned up prerender fallback using cached product data");
|
||
}
|
||
|
||
// Initialize komponenten from cached product data
|
||
const komponenten = [];
|
||
if(cachedData.product.komponenten) {
|
||
for(const komponent of cachedData.product.komponenten.split(",")) {
|
||
// Handle both "x" and "×" as separators
|
||
const [id, count] = komponent.split(/[x×]/);
|
||
komponenten.push({id: id.trim(), count: count.trim()});
|
||
}
|
||
}
|
||
|
||
this.state = {
|
||
product: cachedData.product,
|
||
loading: false,
|
||
upgrading: false,
|
||
error: null,
|
||
attributeImages: {},
|
||
attributes: cachedData.attributes || [],
|
||
isSteckling: false,
|
||
imageDialogOpen: false,
|
||
komponenten: komponenten,
|
||
komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded
|
||
komponentenData: {}, // Store individual komponent data with loading states
|
||
komponentenImages: {}, // Store tiny pictures for komponenten
|
||
totalKomponentenPrice: 0,
|
||
totalSavings: 0,
|
||
// Embedded products from <product> tags in description
|
||
embeddedProducts: {},
|
||
embeddedProductImages: {},
|
||
// Collapsible sections state
|
||
showQuestionForm: false,
|
||
showRatingForm: false,
|
||
showAvailabilityForm: false,
|
||
// Share popper state
|
||
shareAnchorEl: null,
|
||
sharePopperOpen: false,
|
||
// Snackbar state
|
||
snackbarOpen: false,
|
||
snackbarMessage: "",
|
||
snackbarSeverity: "success",
|
||
// Similar products
|
||
similarProducts: cachedData.similarProducts || []
|
||
};
|
||
} else if (partialProduct && isUpgrading) {
|
||
// Partial product data found - enter upgrading state
|
||
console.log("ProductDetailPage: Found partial product data, entering upgrading state");
|
||
|
||
// Clean up prerender fallback since we have some data
|
||
if (typeof window !== "undefined" && window.__PRERENDER_FALLBACK__) {
|
||
delete window.__PRERENDER_FALLBACK__;
|
||
console.log("ProductDetailPage: Cleaned up prerender fallback using partial product data");
|
||
}
|
||
|
||
// Initialize komponenten from partial product data if available
|
||
const komponenten = [];
|
||
if(partialProduct.komponenten) {
|
||
for(const komponent of partialProduct.komponenten.split(",")) {
|
||
// Handle both "x" and "×" as separators
|
||
const [id, count] = komponent.split(/[x×]/);
|
||
komponenten.push({id: id.trim(), count: count.trim()});
|
||
}
|
||
}
|
||
|
||
this.state = {
|
||
product: partialProduct,
|
||
loading: false,
|
||
upgrading: true, // This indicates we have partial data and are loading complete data
|
||
error: null,
|
||
attributeImages: {},
|
||
attributes: [], // Will be loaded when upgrading
|
||
isSteckling: false,
|
||
imageDialogOpen: false,
|
||
komponenten: komponenten,
|
||
komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded
|
||
komponentenData: {}, // Store individual komponent data with loading states
|
||
komponentenImages: {}, // Store tiny pictures for komponenten
|
||
totalKomponentenPrice: 0,
|
||
totalSavings: 0,
|
||
// Embedded products from <product> tags in description
|
||
embeddedProducts: {},
|
||
embeddedProductImages: {},
|
||
// Collapsible sections state
|
||
showQuestionForm: false,
|
||
showRatingForm: false,
|
||
showAvailabilityForm: false,
|
||
// Share popper state
|
||
shareAnchorEl: null,
|
||
sharePopperOpen: false,
|
||
// Snackbar state
|
||
snackbarOpen: false,
|
||
snackbarMessage: "",
|
||
snackbarSeverity: "success",
|
||
// Similar products
|
||
similarProducts: []
|
||
};
|
||
} else {
|
||
// No cached data found - full loading state
|
||
this.state = {
|
||
product: null,
|
||
loading: true,
|
||
upgrading: false,
|
||
error: null,
|
||
attributeImages: {},
|
||
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,
|
||
// Embedded products from <product> tags in description
|
||
embeddedProducts: {},
|
||
embeddedProductImages: {},
|
||
// Collapsible sections state
|
||
showQuestionForm: false,
|
||
showRatingForm: false,
|
||
showAvailabilityForm: false,
|
||
// Share popper state
|
||
shareAnchorEl: null,
|
||
sharePopperOpen: false,
|
||
// Snackbar state
|
||
snackbarOpen: false,
|
||
snackbarMessage: "",
|
||
snackbarSeverity: "success",
|
||
// Similar products
|
||
similarProducts: []
|
||
};
|
||
}
|
||
}
|
||
|
||
componentDidMount() {
|
||
// Update context with cached product if available
|
||
if (this.state.product && this.props.setCurrentProduct) {
|
||
console.log('ProductDetailPage: Setting product context from cache', this.state.product.name);
|
||
this.props.setCurrentProduct({
|
||
name: this.state.product.name,
|
||
categoryId: this.state.product.kategorien ? this.state.product.kategorien.split(',')[0] : undefined
|
||
});
|
||
} else if (this.state.product) {
|
||
console.warn('ProductDetailPage: setCurrentProduct prop is missing despite having product');
|
||
}
|
||
|
||
// Load product data if we have no product or if we're in upgrading state
|
||
if (!this.state.product || this.state.upgrading) {
|
||
this.loadProductData();
|
||
} else {
|
||
// Product is cached, but we still need to load attribute images and komponenten
|
||
if (this.state.attributes && this.state.attributes.length > 0) {
|
||
this.loadAttributeImages(this.state.attributes);
|
||
}
|
||
|
||
if (this.state.komponenten.length > 0 && !this.state.komponentenLoaded) {
|
||
for(const komponent of this.state.komponenten) {
|
||
this.loadKomponent(komponent.id, komponent.count);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
componentDidUpdate(prevProps) {
|
||
// Check for seoName changes
|
||
if (prevProps.seoName !== this.props.seoName) {
|
||
// Clear context when navigating to new product
|
||
if (this.props.setCurrentProduct) {
|
||
this.props.setCurrentProduct(null);
|
||
}
|
||
|
||
this.setState(
|
||
{ product: null, loading: true, upgrading: false, error: null, imageDialogOpen: false, similarProducts: [] },
|
||
this.loadProductData
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Check for language changes
|
||
const prevLanguage = prevProps.languageContext?.currentLanguage || prevProps.i18n?.language || 'de';
|
||
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||
|
||
if (prevLanguage !== currentLanguage) {
|
||
console.log('Language changed from', prevLanguage, 'to', currentLanguage, '- reloading product data');
|
||
|
||
// Clear caches globally to force fresh socket calls with new language
|
||
if (typeof window !== 'undefined') {
|
||
window.productCache = {};
|
||
window.productDetailCache = {};
|
||
}
|
||
|
||
// Reset component state and reload data
|
||
this.setState(
|
||
{
|
||
loading: false,
|
||
upgrading: false,
|
||
error: null,
|
||
imageDialogOpen: false,
|
||
komponentenData: {},
|
||
komponentenLoaded: false,
|
||
totalKomponentenPrice: 0,
|
||
totalSavings: 0,
|
||
similarProducts: []
|
||
},
|
||
this.loadProductData
|
||
);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// Fetch image from server
|
||
console.log('loadKomponentImage', bildId);
|
||
window.socketManager.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 = {};
|
||
}
|
||
|
||
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||
const cacheKey = `product_${id}_${currentLanguage}`;
|
||
|
||
// Check if this komponent is already cached
|
||
if (window.productDetailCache[cacheKey]) {
|
||
const cachedProduct = window.productDetailCache[cacheKey];
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Mark this komponent as loading
|
||
this.setState(prevState => ({
|
||
komponentenData: {
|
||
...prevState.komponentenData,
|
||
[id]: {
|
||
...prevState.komponentenData[id],
|
||
loading: true,
|
||
loaded: false,
|
||
count: parseInt(count)
|
||
}
|
||
}
|
||
}));
|
||
|
||
console.log('loadKomponent', id, count);
|
||
|
||
window.socketManager.emit(
|
||
"getProductView",
|
||
{ articleId: id, language: currentLanguage, requestTranslation: currentLanguage === "de" ? false : true},
|
||
(res) => {
|
||
if (res.success) {
|
||
// Use translated product if available, otherwise use original product
|
||
const productData = res.translatedProduct || res.product;
|
||
|
||
// Cache the successful response
|
||
window.productDetailCache[cacheKey] = productData;
|
||
|
||
// Load komponent image if available
|
||
if (productData.pictureList) {
|
||
this.loadKomponentImage(id, productData.pictureList);
|
||
}
|
||
|
||
// Update state with loaded data
|
||
this.setState(prevState => {
|
||
const newKomponentenData = {
|
||
...prevState.komponentenData,
|
||
[id]: {
|
||
...productData,
|
||
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
|
||
};
|
||
});
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
loadAttributeImages = (attributes) => {
|
||
// Initialize window-level attribute image cache if it doesn't exist
|
||
if (!window.attributeImageCache) {
|
||
window.attributeImageCache = {};
|
||
}
|
||
|
||
if (attributes && attributes.length > 0) {
|
||
const attributeImages = {};
|
||
|
||
for (const attribute of attributes) {
|
||
const cacheKey = attribute.kMerkmalWert;
|
||
|
||
if (attribute.cName == "Anzahl")
|
||
this.setState({ isSteckling: true });
|
||
|
||
// Check if we have a cached result (either URL or negative result)
|
||
if (window.attributeImageCache[cacheKey]) {
|
||
const cached = window.attributeImageCache[cacheKey];
|
||
if (cached.url) {
|
||
// Use cached URL
|
||
attributeImages[cacheKey] = cached.url;
|
||
}
|
||
} else {
|
||
// Not in cache, fetch from server
|
||
console.log('getAttributePicture', cacheKey);
|
||
window.socketManager.emit(
|
||
"getAttributePicture",
|
||
{ id: cacheKey },
|
||
(res) => {
|
||
console.log("getAttributePicture", res);
|
||
if (res.success && !res.noPicture) {
|
||
const blob = new Blob([res.imageBuffer], {
|
||
type: "image/jpeg",
|
||
});
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
// Cache the successful URL
|
||
window.attributeImageCache[cacheKey] = {
|
||
url: url,
|
||
timestamp: Date.now(),
|
||
};
|
||
|
||
// Update state and force re-render
|
||
this.setState(prevState => ({
|
||
attributeImages: {
|
||
...prevState.attributeImages,
|
||
[cacheKey]: url
|
||
}
|
||
}));
|
||
} else {
|
||
// Cache negative result to avoid future requests
|
||
// This handles both failure cases and success with noPicture: true
|
||
window.attributeImageCache[cacheKey] = {
|
||
noImage: true,
|
||
timestamp: Date.now(),
|
||
};
|
||
}
|
||
}
|
||
);
|
||
}
|
||
}
|
||
|
||
// Set initial state with cached images
|
||
if (Object.keys(attributeImages).length > 0) {
|
||
this.setState({ attributeImages });
|
||
}
|
||
}
|
||
}
|
||
|
||
loadProductData = () => {
|
||
console.log('loadProductData', this.props.seoName);
|
||
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||
console.log('debuglanguage', currentLanguage);
|
||
|
||
const cacheKey = `product_${this.props.seoName}_${currentLanguage}`;
|
||
|
||
window.socketManager.emit(
|
||
"getProductView",
|
||
{ seoName: this.props.seoName, language: currentLanguage, requestTranslation: currentLanguage === "de" ? false : true},
|
||
(res) => {
|
||
if (res.success) {
|
||
// Use translated product if available, otherwise use original product
|
||
const productData = res.translatedProduct || res.product;
|
||
productData.seoName = this.props.seoName;
|
||
|
||
// Initialize cache if it doesn't exist
|
||
if (!window.productDetailCache) {
|
||
window.productDetailCache = {};
|
||
}
|
||
|
||
// Cache the complete response data (product + attributes) - cache the response with translated product
|
||
const cacheData = { ...res, product: productData };
|
||
window.productDetailCache[cacheKey] = cacheData;
|
||
|
||
// Clean up prerender fallback since we now have real data
|
||
if (typeof window !== "undefined" && window.__PRERENDER_FALLBACK__) {
|
||
delete window.__PRERENDER_FALLBACK__;
|
||
console.log("ProductDetailPage: Cleaned up prerender fallback after loading product data");
|
||
}
|
||
|
||
const komponenten = [];
|
||
if(productData.komponenten) {
|
||
for(const komponent of productData.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: productData,
|
||
loading: false,
|
||
upgrading: false, // Clear upgrading state since we now have complete data
|
||
error: null,
|
||
imageDialogOpen: false,
|
||
attributes: res.attributes,
|
||
komponenten: komponenten,
|
||
komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded
|
||
similarProducts: res.similarProducts || []
|
||
}, () => {
|
||
// Update context
|
||
if (this.props.setCurrentProduct) {
|
||
console.log('ProductDetailPage: Setting product context from fetch', productData.name);
|
||
this.props.setCurrentProduct({
|
||
name: productData.name,
|
||
categoryId: productData.kategorien ? productData.kategorien.split(',')[0] : undefined
|
||
});
|
||
} else {
|
||
console.warn('ProductDetailPage: setCurrentProduct prop is missing after fetch');
|
||
}
|
||
|
||
if(komponenten.length > 0) {
|
||
for(const komponent of komponenten) {
|
||
this.loadKomponent(komponent.id, komponent.count);
|
||
}
|
||
}
|
||
});
|
||
console.log("getProductView", res);
|
||
|
||
// Load attribute images
|
||
this.loadAttributeImages(res.attributes);
|
||
} else {
|
||
console.error(
|
||
"Error loading product:",
|
||
res.error || "Unknown error",
|
||
res
|
||
);
|
||
this.setState({
|
||
product: null,
|
||
loading: false,
|
||
error: "Error loading product",
|
||
imageDialogOpen: false,
|
||
});
|
||
}
|
||
}
|
||
);
|
||
};
|
||
|
||
handleOpenDialog = () => {
|
||
this.setState({ imageDialogOpen: true });
|
||
};
|
||
|
||
handleCloseDialog = () => {
|
||
this.setState({ imageDialogOpen: false });
|
||
};
|
||
|
||
toggleQuestionForm = () => {
|
||
this.setState(prevState => ({
|
||
showQuestionForm: !prevState.showQuestionForm,
|
||
showRatingForm: false,
|
||
showAvailabilityForm: false
|
||
}), () => {
|
||
if (this.state.showQuestionForm) {
|
||
setTimeout(() => this.scrollToSection('question-form'), 100);
|
||
}
|
||
});
|
||
};
|
||
|
||
toggleRatingForm = () => {
|
||
this.setState(prevState => ({
|
||
showRatingForm: !prevState.showRatingForm,
|
||
showQuestionForm: false,
|
||
showAvailabilityForm: false
|
||
}), () => {
|
||
if (this.state.showRatingForm) {
|
||
setTimeout(() => this.scrollToSection('rating-form'), 100);
|
||
}
|
||
});
|
||
};
|
||
|
||
toggleAvailabilityForm = () => {
|
||
this.setState(prevState => ({
|
||
showAvailabilityForm: !prevState.showAvailabilityForm,
|
||
showQuestionForm: false,
|
||
showRatingForm: false
|
||
}), () => {
|
||
if (this.state.showAvailabilityForm) {
|
||
setTimeout(() => this.scrollToSection('availability-form'), 100);
|
||
}
|
||
});
|
||
};
|
||
|
||
scrollToSection = (sectionId) => {
|
||
const element = document.getElementById(sectionId);
|
||
if (element) {
|
||
element.scrollIntoView({
|
||
behavior: 'smooth',
|
||
block: 'start'
|
||
});
|
||
}
|
||
};
|
||
|
||
// Share functionality
|
||
handleShareClick = (event) => {
|
||
this.setState({
|
||
shareAnchorEl: event.currentTarget,
|
||
sharePopperOpen: true
|
||
});
|
||
};
|
||
|
||
handleShareClose = () => {
|
||
this.setState({
|
||
shareAnchorEl: null,
|
||
sharePopperOpen: false
|
||
});
|
||
};
|
||
|
||
showSnackbar = (message, severity = "success") => {
|
||
this.setState({
|
||
snackbarOpen: true,
|
||
snackbarMessage: message,
|
||
snackbarSeverity: severity
|
||
});
|
||
};
|
||
|
||
handleSnackbarClose = (event, reason) => {
|
||
if (reason === 'clickaway') {
|
||
return;
|
||
}
|
||
this.setState({ snackbarOpen: false });
|
||
};
|
||
|
||
getProductUrl = () => {
|
||
return `${window.location.origin}/Artikel/${this.props.seoName}`;
|
||
};
|
||
|
||
handleEmbedShare = () => {
|
||
const embedCode = `<iframe src="${this.getProductUrl()}" width="100%" height="600" frameborder="0"></iframe>`;
|
||
navigator.clipboard.writeText(embedCode).then(() => {
|
||
this.showSnackbar("Einbettungscode wurde in die Zwischenablage kopiert!");
|
||
}).catch(() => {
|
||
// Fallback for older browsers
|
||
try {
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = embedCode;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textArea);
|
||
this.showSnackbar("Einbettungscode wurde in die Zwischenablage kopiert!");
|
||
} catch {
|
||
this.showSnackbar("Fehler beim Kopieren des Einbettungscodes", "error");
|
||
}
|
||
});
|
||
this.handleShareClose();
|
||
};
|
||
|
||
handleWhatsAppShare = () => {
|
||
const url = this.getProductUrl();
|
||
const text = `Schau dir dieses Produkt an: ${cleanProductName(this.state.product.name)}`;
|
||
const whatsappUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`;
|
||
window.open(whatsappUrl, '_blank');
|
||
this.handleShareClose();
|
||
};
|
||
|
||
handleFacebookShare = () => {
|
||
const url = this.getProductUrl();
|
||
const facebookUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`;
|
||
window.open(facebookUrl, '_blank');
|
||
this.handleShareClose();
|
||
};
|
||
|
||
handleTelegramShare = () => {
|
||
const url = this.getProductUrl();
|
||
const text = `Schau dir dieses Produkt an: ${cleanProductName(this.state.product.name)}`;
|
||
const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`;
|
||
window.open(telegramUrl, '_blank');
|
||
this.handleShareClose();
|
||
};
|
||
|
||
handleEmailShare = () => {
|
||
const url = this.getProductUrl();
|
||
const subject = `Produktempfehlung: ${cleanProductName(this.state.product.name)}`;
|
||
const body = `Hallo,\n\nich möchte dir dieses Produkt empfehlen:\n\n${cleanProductName(this.state.product.name)}\n${url}\n\nViele Grüße`;
|
||
const emailUrl = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||
window.location.href = emailUrl;
|
||
this.handleShareClose();
|
||
};
|
||
|
||
handleLinkCopy = () => {
|
||
const url = this.getProductUrl();
|
||
navigator.clipboard.writeText(url).then(() => {
|
||
this.showSnackbar("Link wurde in die Zwischenablage kopiert!");
|
||
}).catch(() => {
|
||
// Fallback for older browsers
|
||
try {
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = url;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textArea);
|
||
this.showSnackbar("Link wurde in die Zwischenablage kopiert!");
|
||
} catch {
|
||
this.showSnackbar("Fehler beim Kopieren des Links", "error");
|
||
}
|
||
});
|
||
this.handleShareClose();
|
||
};
|
||
|
||
// Render embedded product from <product articlenr="..."> tag in description
|
||
renderEmbeddedProduct = (articleNr) => {
|
||
console.log('renderEmbeddedProduct called with articleNr:', articleNr);
|
||
|
||
// Check if we already have this product data in state
|
||
const embeddedProducts = this.state.embeddedProducts || {};
|
||
const productData = embeddedProducts[articleNr];
|
||
|
||
console.log('Embedded product data:', productData);
|
||
|
||
// If there was an error loading, show error message (don't retry infinitely)
|
||
if (productData && productData.error) {
|
||
return (
|
||
<Box
|
||
key={`embedded-${articleNr}`}
|
||
sx={{
|
||
my: 2,
|
||
p: 2,
|
||
background: "#fff3f3",
|
||
borderRadius: 2,
|
||
border: "1px solid #ffcdd2"
|
||
}}
|
||
>
|
||
<Typography variant="body2" color="error">
|
||
Produkt nicht gefunden (Artikelnr: {articleNr})
|
||
</Typography>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
if (!productData || !productData.loaded) {
|
||
// If not loaded yet and not currently loading, fetch it
|
||
if (!productData || (!productData.loading && !productData.error)) {
|
||
console.log('Starting to load embedded product:', articleNr);
|
||
this.loadEmbeddedProduct(articleNr);
|
||
}
|
||
|
||
// Return loading state
|
||
return (
|
||
<Box
|
||
key={`embedded-${articleNr}`}
|
||
sx={{
|
||
my: 2,
|
||
p: 2,
|
||
background: "#f9f9f9",
|
||
borderRadius: 2,
|
||
border: "1px solid #e0e0e0"
|
||
}}
|
||
>
|
||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||
<Box sx={{ width: 60, height: 60, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||
{/* Empty placeholder for image */}
|
||
</Box>
|
||
<Box>
|
||
<Typography variant="body1">
|
||
{this.props.t('product.loadingProduct')}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{this.props.t('product.articleNumber')}: {articleNr}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
// Product data is loaded, render it
|
||
const embeddedImages = this.state.embeddedProductImages || {};
|
||
const productImage = embeddedImages[articleNr];
|
||
|
||
return (
|
||
<Box
|
||
key={`embedded-${articleNr}`}
|
||
component={Link}
|
||
to={`/Artikel/${productData.seoName}`}
|
||
sx={{
|
||
display: "block",
|
||
my: 2,
|
||
p: 2,
|
||
background: "#f9f9f9",
|
||
borderRadius: 2,
|
||
border: "1px solid #e0e0e0",
|
||
textDecoration: "none",
|
||
color: "inherit",
|
||
transition: "all 0.2s",
|
||
"&:hover": {
|
||
backgroundColor: "#f0f0f0",
|
||
boxShadow: "0 2px 8px rgba(0,0,0,0.1)"
|
||
}
|
||
}}
|
||
>
|
||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||
<Box sx={{ width: 60, height: 60, flexShrink: 0 }}>
|
||
{productImage ? (
|
||
<CardMedia
|
||
component="img"
|
||
height="60"
|
||
image={productImage}
|
||
alt={productData.name}
|
||
sx={{
|
||
objectFit: "contain",
|
||
borderRadius: 1,
|
||
border: "1px solid #e0e0e0"
|
||
}}
|
||
/>
|
||
) : (
|
||
<CardMedia
|
||
component="img"
|
||
height="60"
|
||
image="/assets/images/nopicture.jpg"
|
||
alt={productData.name}
|
||
sx={{
|
||
objectFit: "contain",
|
||
borderRadius: 1,
|
||
border: "1px solid #e0e0e0"
|
||
}}
|
||
/>
|
||
)}
|
||
</Box>
|
||
<Box sx={{ flex: 1 }}>
|
||
<Typography variant="body1" sx={{ fontWeight: 500, mb: 0.5 }}>
|
||
{cleanProductName(productData.name)}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{this.props.t('product.articleNumber')}: {productData.articleNumber}
|
||
</Typography>
|
||
</Box>
|
||
<Box sx={{ textAlign: "right" }}>
|
||
<Typography variant="h6" sx={{ fontWeight: 600, color: "primary.main" }}>
|
||
{new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(productData.price)}
|
||
</Typography>
|
||
<Typography variant="caption" color="text.secondary">
|
||
inkl. MwSt.
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
// Load embedded product data by article number
|
||
loadEmbeddedProduct = (articleNr) => {
|
||
console.log('loadEmbeddedProduct', articleNr);
|
||
|
||
// Mark as loading
|
||
this.setState(prevState => ({
|
||
embeddedProducts: {
|
||
...prevState.embeddedProducts,
|
||
[articleNr]: { loading: true, loaded: false }
|
||
}
|
||
}));
|
||
|
||
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||
|
||
// Fetch product data from API using getProductView (same as komponenten)
|
||
window.socketManager.emit('getProductView', {
|
||
articleNr: articleNr,
|
||
language: currentLanguage,
|
||
requestTranslation: currentLanguage === 'de' ? false : true
|
||
}, (response) => {
|
||
console.log('loadEmbeddedProduct response:', articleNr, response);
|
||
|
||
if (response.success && response.product) {
|
||
// Use translated product if available, otherwise use original product
|
||
const product = response.translatedProduct || response.product;
|
||
|
||
console.log('Successfully loaded embedded product:', articleNr, product.name);
|
||
|
||
// Update state with loaded product data
|
||
this.setState(prevState => ({
|
||
embeddedProducts: {
|
||
...prevState.embeddedProducts,
|
||
[articleNr]: {
|
||
...product,
|
||
loading: false,
|
||
loaded: true
|
||
}
|
||
}
|
||
}));
|
||
|
||
// Load product image if available
|
||
if (product.pictureList && product.pictureList.length > 0) {
|
||
const bildId = product.pictureList.split(',')[0];
|
||
this.loadEmbeddedProductImage(articleNr, bildId);
|
||
}
|
||
} else {
|
||
console.warn(`Failed to load embedded product ${articleNr}:`, response);
|
||
// Mark as failed to load
|
||
this.setState(prevState => ({
|
||
embeddedProducts: {
|
||
...prevState.embeddedProducts,
|
||
[articleNr]: {
|
||
loading: false,
|
||
loaded: false,
|
||
error: true,
|
||
errorMessage: response.error || 'Unknown error'
|
||
}
|
||
}
|
||
}));
|
||
}
|
||
});
|
||
};
|
||
|
||
// Load embedded product image
|
||
loadEmbeddedProductImage = (articleNr, bildId) => {
|
||
console.log('loadEmbeddedProductImage', articleNr, bildId);
|
||
|
||
window.socketManager.emit('getPic', { bildId, size: 'small' }, (res) => {
|
||
console.log('loadEmbeddedProductImage response:', articleNr, res.success);
|
||
|
||
if (res.success) {
|
||
const imageUrl = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
||
this.setState(prevState => {
|
||
console.log('Setting embedded product image for', articleNr);
|
||
return {
|
||
embeddedProductImages: {
|
||
...prevState.embeddedProductImages,
|
||
[articleNr]: imageUrl
|
||
}
|
||
};
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
componentWillUnmount() {
|
||
if (this.props.setCurrentProduct) {
|
||
this.props.setCurrentProduct(null);
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const { product, loading, upgrading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings, shareAnchorEl, sharePopperOpen, snackbarOpen, snackbarMessage, snackbarSeverity } =
|
||
this.state;
|
||
|
||
// Debug alerts removed
|
||
|
||
|
||
|
||
if (loading && !upgrading) {
|
||
// Only show full loading screen when we have no product data at all
|
||
// Check if prerender fallback is available
|
||
if (typeof window !== "undefined" && window.__PRERENDER_FALLBACK__) {
|
||
return (
|
||
<div
|
||
dangerouslySetInnerHTML={{
|
||
__html: window.__PRERENDER_FALLBACK__.content,
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// Fallback to blank page if no prerender content
|
||
return <div style={{ minHeight: "60vh" }} />;
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<Box sx={{ p: 4, textAlign: "center" }}>
|
||
<Typography variant="h2" component="h2" gutterBottom color="error">
|
||
Fehler
|
||
</Typography>
|
||
<Typography>{error}</Typography>
|
||
<Link to="/" style={{ textDecoration: "none" }}>
|
||
<Typography color="primary" sx={{ mt: 2 }}>
|
||
{this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'}
|
||
</Typography>
|
||
</Link>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
if (!product) {
|
||
return (
|
||
<Box sx={{ p: 4, textAlign: "center" }}>
|
||
<Typography variant="h2" component="h2" gutterBottom>
|
||
Produkt nicht gefunden
|
||
</Typography>
|
||
<Typography>
|
||
Das gesuchte Produkt existiert nicht oder wurde entfernt.
|
||
</Typography>
|
||
<Link to="/" style={{ textDecoration: "none" }}>
|
||
<Typography color="primary" sx={{ mt: 2 }}>
|
||
{this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'}
|
||
</Typography>
|
||
</Link>
|
||
</Box>
|
||
);
|
||
}
|
||
// Format price with tax
|
||
const priceWithTax = new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(product.price);
|
||
|
||
let originalPriceWithTax = null;
|
||
if (product.rebate && product.rebate > 0) {
|
||
const rebatePct = product.rebate / 100;
|
||
const originalPrice = Math.round((product.price / (1 - rebatePct)) * 10) / 10;
|
||
originalPriceWithTax = new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(originalPrice);
|
||
}
|
||
|
||
return (
|
||
<Container
|
||
maxWidth="lg"
|
||
sx={{
|
||
p: { xs: 2, md: 2 },
|
||
pb: { xs: 4, md: 8 },
|
||
flexGrow: 1
|
||
}}
|
||
>
|
||
{/* Breadcrumbs */}
|
||
<Box
|
||
sx={{
|
||
mb: 2,
|
||
position: ["-webkit-sticky", "sticky"], // Provide both prefixed and standard
|
||
top: {
|
||
xs: "110px",
|
||
sm: "110px",
|
||
md: "110px",
|
||
lg: "110px",
|
||
} /* Offset to sit below the header 120 mith menu for md and lg*/,
|
||
left: 0,
|
||
width: "100%",
|
||
display: "flex",
|
||
zIndex: (theme) =>
|
||
theme.zIndex.appBar - 1 /* Just below the AppBar */,
|
||
py: 0,
|
||
px: 2,
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
ml: { xs: 0, md: 0 },
|
||
display: "inline-flex",
|
||
px: 0,
|
||
py: 1,
|
||
backgroundColor: "#2e7d32", //primary dark green
|
||
borderRadius: 1,
|
||
}}
|
||
>
|
||
<Typography variant="body2" color="text.secondary">
|
||
<Link
|
||
to="/"
|
||
onClick={() => this.props.navigate(-1)}
|
||
style={{
|
||
paddingLeft: 16,
|
||
paddingRight: 16,
|
||
paddingTop: 8,
|
||
paddingBottom: 8,
|
||
textDecoration: "none",
|
||
color: "#fff",
|
||
fontWeight: "bold",
|
||
}}
|
||
>
|
||
{this.props.t ? this.props.t('common.back') : 'Zurück'}
|
||
</Link>
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: { xs: "column", md: "row" },
|
||
gap: 4,
|
||
background: "#fff",
|
||
borderRadius: 2,
|
||
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
||
}}
|
||
>
|
||
<ProductImage
|
||
product={product}
|
||
fullscreenOpen={this.state.imageDialogOpen}
|
||
onOpenFullscreen={this.handleOpenDialog}
|
||
onCloseFullscreen={this.handleCloseDialog}
|
||
/>
|
||
|
||
{/* Product Details */}
|
||
<Box
|
||
sx={{
|
||
flex: "1 1 60%",
|
||
p: { xs: 2, md: 4 },
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
{/* Product identifiers */}
|
||
<Box sx={{ mb: 1 }}>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{this.props.t ? this.props.t('product.articleNumber') : 'Artikelnummer'}: {product.articleNumber} {product.gtin ? ` | GTIN: ${product.gtin}` : ""}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* Product title */}
|
||
<Typography
|
||
variant="h4"
|
||
component="h1"
|
||
gutterBottom
|
||
sx={{ fontWeight: 600, color: "#333" }}
|
||
>
|
||
{cleanProductName(product.name)}
|
||
</Typography>
|
||
|
||
{/* Manufacturer if available */}
|
||
{product.manufacturer && (
|
||
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
|
||
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
|
||
{this.props.t ? this.props.t('product.manufacturer') : 'Hersteller'}:
|
||
<Button
|
||
variant="text"
|
||
size="small"
|
||
onClick={() => {
|
||
if (this.props.navigate) {
|
||
this.props.navigate(`/search?q=${encodeURIComponent(product.manufacturer)}`);
|
||
}
|
||
}}
|
||
sx={{
|
||
ml: 0.5,
|
||
p: 0,
|
||
minWidth: 'auto',
|
||
height: 'auto',
|
||
fontSize: 'inherit',
|
||
fontStyle: 'inherit',
|
||
textTransform: 'none',
|
||
'&:hover': {
|
||
backgroundColor: 'transparent',
|
||
textDecoration: 'underline',
|
||
}
|
||
}}
|
||
>
|
||
{product.manufacturer}
|
||
</Button>
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Attribute images and chips with action buttons */}
|
||
{(attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert])) && (
|
||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2, gap: 2 }}>
|
||
<Stack direction="row" spacing={0} sx={{ flexWrap: "wrap", gap: 1, flex: 1 }}>
|
||
{attributes
|
||
.filter(attribute => attributeImages[attribute.kMerkmalWert])
|
||
.map((attribute) => {
|
||
const key = attribute.kMerkmalWert;
|
||
return (
|
||
<Box key={key} sx={{ mb: 1,border: "1px solid #e0e0e0", borderRadius: 1 }}>
|
||
<CardMedia
|
||
component="img"
|
||
style={{ width: "72px", height: "98px" }}
|
||
image={attributeImages[key]}
|
||
alt={`Attribute ${key}`}
|
||
/>
|
||
</Box>
|
||
);
|
||
})}
|
||
{attributes
|
||
.filter(attribute => !attributeImages[attribute.kMerkmalWert])
|
||
.map((attribute) => (
|
||
<Chip
|
||
key={attribute.kMerkmalWert}
|
||
label={attribute.cWert}
|
||
disabled
|
||
sx={{
|
||
'&.Mui-disabled': {
|
||
opacity: 1, // ← Remove the "fog"
|
||
},
|
||
'& .MuiChip-label': {
|
||
fontWeight: 'bold',
|
||
color: 'inherit', // ← Keep normal text color
|
||
},
|
||
}}
|
||
/>
|
||
))}
|
||
</Stack>
|
||
|
||
{/* Right-aligned action buttons */}
|
||
<Stack direction="column" spacing={1} sx={{ flexShrink: 0 }}>
|
||
<Button
|
||
variant="outlined"
|
||
size="small"
|
||
onClick={this.toggleQuestionForm}
|
||
sx={{
|
||
fontSize: "0.75rem",
|
||
px: 1.5,
|
||
py: 0.5,
|
||
minWidth: "auto",
|
||
whiteSpace: "nowrap"
|
||
}}
|
||
>
|
||
Frage zum Artikel
|
||
</Button>
|
||
<Button
|
||
variant="outlined"
|
||
size="small"
|
||
onClick={this.toggleRatingForm}
|
||
sx={{
|
||
fontSize: "0.75rem",
|
||
px: 1.5,
|
||
py: 0.5,
|
||
minWidth: "auto",
|
||
whiteSpace: "nowrap"
|
||
}}
|
||
>
|
||
Artikel Bewerten
|
||
</Button>
|
||
{(product.available !== 1 && product.availableSupplier !== 1) && (
|
||
<Button
|
||
variant="outlined"
|
||
size="small"
|
||
onClick={this.toggleAvailabilityForm}
|
||
sx={{
|
||
fontSize: "0.75rem",
|
||
px: 1.5,
|
||
py: 0.5,
|
||
minWidth: "auto",
|
||
whiteSpace: "nowrap",
|
||
borderColor: "warning.main",
|
||
color: "warning.main",
|
||
"&:hover": {
|
||
borderColor: "warning.dark",
|
||
backgroundColor: "warning.light"
|
||
}
|
||
}}
|
||
>
|
||
Verfügbarkeit anfragen
|
||
</Button>
|
||
)}
|
||
</Stack>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Weight */}
|
||
{product.weight > 0 && (
|
||
<Box sx={{ mb: 2 }}>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{this.props.t ? this.props.t('product.weight', { weight: product.weight.toFixed(1).replace(".", ",") }) : `Gewicht: ${product.weight.toFixed(1).replace(".", ",")} kg`}
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Price and availability section */}
|
||
<Box
|
||
sx={{
|
||
mt: "auto",
|
||
p: 3,
|
||
background: "#f9f9f9",
|
||
borderRadius: 2,
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: { xs: "column", sm: "row" },
|
||
justifyContent: "space-between",
|
||
alignItems: { xs: "flex-start", sm: "flex-start" },
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Box>
|
||
<Typography
|
||
variant="h5"
|
||
color="primary"
|
||
sx={{ fontWeight: "bold" }}
|
||
>
|
||
{originalPriceWithTax && (
|
||
<Typography
|
||
component="span"
|
||
variant="h6"
|
||
sx={{
|
||
textDecoration: 'line-through',
|
||
color: 'text.secondary',
|
||
mr: 1,
|
||
display: 'inline-block'
|
||
}}
|
||
>
|
||
{originalPriceWithTax}
|
||
</Typography>
|
||
)}
|
||
{priceWithTax}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{this.props.t ? this.props.t('product.inclVat', { vat: product.vat }) : `inkl. ${product.vat}% MwSt.`}
|
||
{product.cGrundEinheit && product.fGrundPreis && (
|
||
<>; {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(product.fGrundPreis)}/{product.cGrundEinheit}</>
|
||
)}
|
||
</Typography>
|
||
|
||
{product.versandklasse &&
|
||
product.versandklasse != "standard" &&
|
||
product.versandklasse != "kostenlos" && (
|
||
<Typography variant="body2" color="text.secondary">
|
||
{product.versandklasse}
|
||
</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"
|
||
}}
|
||
>
|
||
{this.props.t ? this.props.t('product.youSave', {
|
||
amount: new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(totalKomponentenPrice - product.price)
|
||
}) : `Sie sparen: ${new Intl.NumberFormat("de-DE", {
|
||
style: "currency",
|
||
currency: "EUR",
|
||
}).format(totalKomponentenPrice - product.price)}`}
|
||
</Typography>
|
||
<Typography variant="caption" color="text.secondary">
|
||
{this.props.t ? this.props.t('product.cheaperThanIndividual') : 'Günstiger als Einzelkauf'}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: { xs: "column", sm: "row" },
|
||
gap: 2,
|
||
alignItems: "flex-start",
|
||
}}
|
||
>
|
||
{isSteckling && product.available == 1 && (
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
<AddToCartButton
|
||
steckling={true}
|
||
cartButton={true}
|
||
seoName={product.seoName}
|
||
pictureList={product.pictureList}
|
||
available={product.available}
|
||
id={product.id + "steckling"}
|
||
price={0}
|
||
vat={product.vat}
|
||
weight={product.weight}
|
||
availableSupplier={product.availableSupplier}
|
||
komponenten={product.komponenten}
|
||
name={cleanProductName(product.name) + " Stecklingsvorbestellung 1 Stück"}
|
||
versandklasse={"nur Abholung"}
|
||
/>
|
||
<Typography
|
||
variant="caption"
|
||
sx={{
|
||
fontStyle: "italic",
|
||
color: "text.secondary",
|
||
textAlign: "center",
|
||
mt: 1
|
||
}}
|
||
>
|
||
{this.props.t ? this.props.t('product.pickupPrice') : 'Abholpreis: 19,90 € pro Steckling.'}
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
<AddToCartButton
|
||
cartButton={true}
|
||
seoName={product.seoName}
|
||
pictureList={product.pictureList}
|
||
available={product.available}
|
||
id={product.id}
|
||
availableSupplier={product.availableSupplier}
|
||
komponenten={product.komponenten}
|
||
cGrundEinheit={product.cGrundEinheit}
|
||
fGrundPreis={product.fGrundPreis}
|
||
price={product.price}
|
||
vat={product.vat}
|
||
weight={product.weight}
|
||
name={cleanProductName(product.name)}
|
||
versandklasse={product.versandklasse}
|
||
/>
|
||
|
||
<Typography
|
||
variant="caption"
|
||
sx={{
|
||
fontStyle: "italic",
|
||
color: "text.secondary",
|
||
textAlign: "center",
|
||
mt: 1
|
||
}}
|
||
>
|
||
{product.id.toString().endsWith("steckling") ?
|
||
(this.props.t ? this.props.t('delivery.times.cutting14Days') : "Lieferzeit: 14 Tage") :
|
||
product.available == 1 ?
|
||
(this.props.t ? this.props.t('delivery.times.standard2to3Days') : "Lieferzeit: 2-3 Tage") :
|
||
product.availableSupplier == 1 ?
|
||
(this.props.t ? this.props.t('delivery.times.supplier7to9Days') : "Lieferzeit: 7-9 Tage") : ""}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Product full description */}
|
||
{(product.description || upgrading) && (
|
||
<Box
|
||
sx={{
|
||
mt: 4,
|
||
p: 4,
|
||
background: "#fff",
|
||
borderRadius: 2,
|
||
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
||
position: "relative",
|
||
}}
|
||
>
|
||
{/* Share button */}
|
||
<Button
|
||
onClick={this.handleShareClick}
|
||
startIcon={<ShareIcon fontSize="small" />}
|
||
sx={{
|
||
position: "absolute",
|
||
top: 8,
|
||
right: 8,
|
||
backgroundColor: "#f5f5f5",
|
||
"&:hover": {
|
||
backgroundColor: "#e0e0e0",
|
||
},
|
||
zIndex: 1,
|
||
fontSize: "0.75rem",
|
||
px: 1.5,
|
||
py: 0.5,
|
||
minWidth: "auto",
|
||
textTransform: "none",
|
||
}}
|
||
size="small"
|
||
>
|
||
Teilen
|
||
</Button>
|
||
<Box
|
||
sx={{
|
||
mt: 2,
|
||
lineHeight: 1.7,
|
||
"& p": { mt: 0, mb: 2 },
|
||
"& strong": { fontWeight: 600 },
|
||
}}
|
||
>
|
||
{product.description ? (() => {
|
||
try {
|
||
// Sanitize HTML to remove invalid tags, but preserve style attributes and <product> tags
|
||
const sanitized = sanitizeHtml(product.description, {
|
||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'product']),
|
||
allowedAttributes: {
|
||
'*': ['class', 'style'],
|
||
'a': ['href', 'title'],
|
||
'img': ['src', 'alt', 'width', 'height'],
|
||
'product': ['articlenr']
|
||
},
|
||
disallowedTagsMode: 'discard'
|
||
});
|
||
|
||
// Parse with custom replace function to handle <product> tags
|
||
return parse(sanitized, {
|
||
replace: (domNode) => {
|
||
if (domNode.type === 'tag' && domNode.name === 'product') {
|
||
const articleNr = domNode.attribs && domNode.attribs['articlenr'];
|
||
if (articleNr) {
|
||
// Render embedded product component
|
||
return this.renderEmbeddedProduct(articleNr);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.warn('Failed to parse product description HTML:', error);
|
||
// Fallback to rendering as plain text if HTML parsing fails
|
||
return <span>{product.description}</span>;
|
||
}
|
||
})() : upgrading ? (
|
||
<Box sx={{ textAlign: "center", py: 2 }}>
|
||
<Typography variant="body1" color="text.secondary">
|
||
{this.props.t ? this.props.t('product.loadingDescription') : 'Produktbeschreibung wird geladen...'}
|
||
</Typography>
|
||
</Box>
|
||
) : null}
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Share Popper */}
|
||
<Popper
|
||
open={sharePopperOpen}
|
||
anchorEl={shareAnchorEl}
|
||
placement="bottom-end"
|
||
sx={{ zIndex: 1300 }}
|
||
>
|
||
<ClickAwayListener onClickAway={this.handleShareClose}>
|
||
<Box
|
||
sx={{
|
||
bgcolor: "background.paper",
|
||
border: "1px solid #ccc",
|
||
borderRadius: 1,
|
||
boxShadow: "0 4px 8px rgba(0,0,0,0.1)",
|
||
minWidth: 200,
|
||
}}
|
||
>
|
||
<MenuList>
|
||
<MenuItem onClick={this.handleEmbedShare}>
|
||
<ListItemIcon>
|
||
<CodeIcon fontSize="small" />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Einbetten" />
|
||
</MenuItem>
|
||
<MenuItem onClick={this.handleWhatsAppShare}>
|
||
<ListItemIcon>
|
||
<WhatsAppIcon fontSize="small" sx={{ color: "#25D366" }} />
|
||
</ListItemIcon>
|
||
<ListItemText primary="WhatsApp" />
|
||
</MenuItem>
|
||
<MenuItem onClick={this.handleTelegramShare}>
|
||
<ListItemIcon>
|
||
<TelegramIcon fontSize="small" sx={{ color: "#0088CC" }} />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Telegram" />
|
||
</MenuItem>
|
||
<MenuItem onClick={this.handleFacebookShare}>
|
||
<ListItemIcon>
|
||
<FacebookIcon fontSize="small" sx={{ color: "#1877F2" }} />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Facebook" />
|
||
</MenuItem>
|
||
<MenuItem onClick={this.handleEmailShare}>
|
||
<ListItemIcon>
|
||
<EmailIcon fontSize="small" />
|
||
</ListItemIcon>
|
||
<ListItemText primary="E-Mail" />
|
||
</MenuItem>
|
||
<MenuItem onClick={this.handleLinkCopy}>
|
||
<ListItemIcon>
|
||
<LinkIcon fontSize="small" />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Link kopieren" />
|
||
</MenuItem>
|
||
</MenuList>
|
||
</Box>
|
||
</ClickAwayListener>
|
||
</Popper>
|
||
|
||
{/* Article Question Form */}
|
||
<Collapse in={this.state.showQuestionForm}>
|
||
<div id="question-form">
|
||
<ArticleQuestionForm
|
||
productId={product.id}
|
||
productName={cleanProductName(product.name)}
|
||
/>
|
||
</div>
|
||
</Collapse>
|
||
|
||
{/* Article Rating Form */}
|
||
<Collapse in={this.state.showRatingForm}>
|
||
<div id="rating-form">
|
||
<ArticleRatingForm
|
||
productId={product.id}
|
||
productName={cleanProductName(product.name)}
|
||
/>
|
||
</div>
|
||
</Collapse>
|
||
|
||
{/* Article Availability Form - only show for out of stock items */}
|
||
{(product.available !== 1 && product.availableSupplier !== 1) && (
|
||
<Collapse in={this.state.showAvailabilityForm}>
|
||
<ArticleAvailabilityForm
|
||
productId={product.id}
|
||
productName={cleanProductName(product.name)}
|
||
/>
|
||
</Collapse>
|
||
)}
|
||
|
||
{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="h2" component="h2" gutterBottom>{this.props.t ? this.props.t('product.consistsOf') : '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">
|
||
{this.props.t ? this.props.t('product.individualPriceTotal') : '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">
|
||
{this.props.t ? this.props.t('product.setPrice') : '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" }}>
|
||
{this.props.t ? this.props.t('product.yourSavings') : '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">
|
||
{this.props.t ? this.props.t('product.loadingComponentDetails', { index: index + 1 }) : `${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>
|
||
)}
|
||
|
||
{/* Similar Products Section */}
|
||
{this.state.similarProducts && this.state.similarProducts.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" component="h2" gutterBottom sx={{ mb: 3 }}>
|
||
{this.props.t ? this.props.t('product.similarProducts') : 'Ähnliche Produkte'}
|
||
</Typography>
|
||
<Box sx={{
|
||
display: "grid",
|
||
gridTemplateColumns: {
|
||
xs: "1fr",
|
||
sm: "repeat(2, 1fr)",
|
||
md: "repeat(3, 1fr)",
|
||
lg: "repeat(4, 1fr)"
|
||
},
|
||
gap: 2
|
||
}}>
|
||
{this.state.similarProducts.map((similarProductData, index) => {
|
||
const product = similarProductData.product;
|
||
return (
|
||
<Box key={product.id} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||
<Product
|
||
id={product.id}
|
||
name={product.name}
|
||
seoName={product.seoName}
|
||
price={product.price}
|
||
currency={product.currency}
|
||
available={product.available}
|
||
manufacturer={product.manufacturer}
|
||
vat={product.vat}
|
||
cGrundEinheit={product.cGrundEinheit}
|
||
fGrundPreis={product.fGrundPreis}
|
||
incoming={product.incomingDate}
|
||
neu={product.neu}
|
||
thc={product.thc}
|
||
floweringWeeks={product.floweringWeeks}
|
||
versandklasse={product.versandklasse}
|
||
weight={product.weight}
|
||
pictureList={product.pictureList}
|
||
availableSupplier={product.availableSupplier}
|
||
komponenten={product.komponenten}
|
||
rebate={product.rebate}
|
||
categoryId={product.kategorien ? product.kategorien.split(',')[0] : undefined}
|
||
priority={index < 6 ? 'high' : 'auto'}
|
||
t={this.props.t}
|
||
/>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Snackbar for user feedback */}
|
||
<Snackbar
|
||
open={snackbarOpen}
|
||
autoHideDuration={4000}
|
||
onClose={this.handleSnackbarClose}
|
||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||
>
|
||
<Alert
|
||
onClose={this.handleSnackbarClose}
|
||
severity={snackbarSeverity}
|
||
sx={{ width: '100%' }}
|
||
>
|
||
{snackbarMessage}
|
||
</Alert>
|
||
</Snackbar>
|
||
</Container>
|
||
);
|
||
}
|
||
}
|
||
|
||
export default withI18n()(ProductDetailPage);
|
||
|