feat: add prerender-single-product script for single product rendering with i18n support
This commit is contained in:
202
prerender-single-product.cjs
Normal file
202
prerender-single-product.cjs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
require("@babel/register")({
|
||||||
|
presets: [
|
||||||
|
["@babel/preset-env", { targets: { node: "current" } }],
|
||||||
|
"@babel/preset-react",
|
||||||
|
],
|
||||||
|
extensions: [".js", ".jsx"],
|
||||||
|
ignore: [/node_modules/],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Minimal globals for socket.io-client only - no JSDOM to avoid interference
|
||||||
|
global.window = {}; // Minimal window object for productCache
|
||||||
|
global.navigator = { userAgent: "node.js" };
|
||||||
|
global.URL = require("url").URL;
|
||||||
|
global.Blob = class MockBlob {
|
||||||
|
constructor(data, options) {
|
||||||
|
this.data = data;
|
||||||
|
this.type = options?.type || "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import modules
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const React = require("react");
|
||||||
|
const io = require("socket.io-client");
|
||||||
|
|
||||||
|
// Initialize i18n for prerendering with German as default
|
||||||
|
const i18n = require("i18next");
|
||||||
|
const { initReactI18next } = require("react-i18next");
|
||||||
|
|
||||||
|
// Import translation (just German for testing)
|
||||||
|
const translationDE = require("./src/i18n/locales/de/translation.js").default;
|
||||||
|
|
||||||
|
// Initialize i18n
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
resources: {
|
||||||
|
de: { translation: translationDE }
|
||||||
|
},
|
||||||
|
lng: 'de',
|
||||||
|
fallbackLng: 'de',
|
||||||
|
debug: false,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false
|
||||||
|
},
|
||||||
|
react: {
|
||||||
|
useSuspense: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make i18n available globally
|
||||||
|
global.i18n = i18n;
|
||||||
|
|
||||||
|
// Import prerender modules
|
||||||
|
const config = require("./prerender/config.cjs");
|
||||||
|
const shopConfig = require("./src/config.js").default;
|
||||||
|
const { renderPage } = require("./prerender/renderer.cjs");
|
||||||
|
const { generateProductMetaTags, generateProductJsonLd } = require("./prerender/seo.cjs");
|
||||||
|
const { fetchProductDetails } = require("./prerender/data-fetching.cjs");
|
||||||
|
|
||||||
|
// Import product component
|
||||||
|
const PrerenderProduct = require("./src/PrerenderProduct.js").default;
|
||||||
|
|
||||||
|
const renderSingleProduct = async (productSeoName) => {
|
||||||
|
const socketUrl = "http://127.0.0.1:9303";
|
||||||
|
console.log(`🔌 Connecting to socket at ${socketUrl}...`);
|
||||||
|
|
||||||
|
const socket = io(socketUrl, {
|
||||||
|
path: "/socket.io/",
|
||||||
|
transports: ["polling", "websocket"],
|
||||||
|
reconnection: false,
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
console.error("❌ Timeout: Could not connect to backend after 15 seconds");
|
||||||
|
socket.disconnect();
|
||||||
|
reject(new Error("Connection timeout"));
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
socket.on("connect", async () => {
|
||||||
|
console.log(`✅ Socket connected. Fetching product: ${productSeoName}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch product details
|
||||||
|
const productDetails = await fetchProductDetails(socket, productSeoName);
|
||||||
|
console.log(`📦 Product found: ${productDetails.product.name}`);
|
||||||
|
|
||||||
|
// Set up minimal global cache (empty for single product test)
|
||||||
|
global.window.productCache = {};
|
||||||
|
global.productCache = {};
|
||||||
|
|
||||||
|
// Create product component
|
||||||
|
const productComponent = React.createElement(PrerenderProduct, {
|
||||||
|
productData: productDetails,
|
||||||
|
t: global.i18n.t.bind(global.i18n),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate metadata
|
||||||
|
const actualSeoName = productDetails.product.seoName || productSeoName;
|
||||||
|
const filename = `Artikel/${actualSeoName}`;
|
||||||
|
const location = `/Artikel/${actualSeoName}`;
|
||||||
|
const description = `Product "${productDetails.product.name}" (seoName: ${productSeoName})`;
|
||||||
|
|
||||||
|
const metaTags = generateProductMetaTags({
|
||||||
|
...productDetails.product,
|
||||||
|
seoName: actualSeoName,
|
||||||
|
}, shopConfig.baseUrl, shopConfig);
|
||||||
|
|
||||||
|
const jsonLdScript = generateProductJsonLd({
|
||||||
|
...productDetails.product,
|
||||||
|
seoName: actualSeoName,
|
||||||
|
}, shopConfig.baseUrl, shopConfig);
|
||||||
|
|
||||||
|
const combinedMetaTags = metaTags + "\n" + jsonLdScript;
|
||||||
|
|
||||||
|
// Render the page
|
||||||
|
console.log(`🎨 Rendering product page...`);
|
||||||
|
const success = renderPage(
|
||||||
|
productComponent,
|
||||||
|
location,
|
||||||
|
filename,
|
||||||
|
description,
|
||||||
|
combinedMetaTags,
|
||||||
|
true, // needsRouter
|
||||||
|
config,
|
||||||
|
false, // suppressLogs
|
||||||
|
productDetails // productData for cache
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const outputPath = path.resolve(__dirname, config.outputDir, `${filename}.html`);
|
||||||
|
console.log(`✅ Product page rendered successfully!`);
|
||||||
|
console.log(`📄 Output file: ${outputPath}`);
|
||||||
|
console.log(`🌐 Test URL: http://localhost:3000/Artikel/${actualSeoName}`);
|
||||||
|
|
||||||
|
// Show file size
|
||||||
|
if (fs.existsSync(outputPath)) {
|
||||||
|
const stats = fs.statSync(outputPath);
|
||||||
|
console.log(`📊 File size: ${Math.round(stats.size / 1024)}KB`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Failed to render product page`);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
socket.disconnect();
|
||||||
|
resolve(success);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error fetching/rendering product: ${error.message}`);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
socket.disconnect();
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", (err) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
console.error("❌ Socket connection error:", err);
|
||||||
|
console.log("💡 Make sure the backend server is running on http://127.0.0.1:9303");
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("error", (err) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
console.error("❌ Socket error:", err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get product seoName from command line arguments
|
||||||
|
const productSeoName = process.argv[2];
|
||||||
|
|
||||||
|
if (!productSeoName) {
|
||||||
|
console.log("❌ Usage: node prerender-single-product.cjs <product-seo-name>");
|
||||||
|
console.log("📝 Example: node prerender-single-product.cjs led-grow-light-600w");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🚀 Starting single product prerender test...`);
|
||||||
|
console.log(`🎯 Product SEO name: ${productSeoName}`);
|
||||||
|
console.log(`🔧 Mode: ${config.isProduction ? 'PRODUCTION' : 'DEVELOPMENT'}`);
|
||||||
|
console.log(`📁 Output directory: ${config.outputDir}`);
|
||||||
|
|
||||||
|
renderSingleProduct(productSeoName)
|
||||||
|
.then((success) => {
|
||||||
|
if (success) {
|
||||||
|
console.log(`\n🎉 Single product prerender completed successfully!`);
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log(`\n💥 Single product prerender failed!`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`\n💥 Single product prerender failed:`, error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -187,7 +187,7 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
|
|||||||
const imageBuffer = await fetchProductImage(socket, bildId);
|
const imageBuffer = await fetchProductImage(socket, bildId);
|
||||||
|
|
||||||
// If overlay exists, apply it to the image
|
// If overlay exists, apply it to the image
|
||||||
if (fs.existsSync(overlayPath)) {
|
if (false && fs.existsSync(overlayPath)) {
|
||||||
try {
|
try {
|
||||||
// Get image dimensions to center the overlay
|
// Get image dimensions to center the overlay
|
||||||
const baseImage = sharp(Buffer.from(imageBuffer));
|
const baseImage = sharp(Buffer.from(imageBuffer));
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const {
|
|||||||
Chip,
|
Chip,
|
||||||
Stack,
|
Stack,
|
||||||
AppBar,
|
AppBar,
|
||||||
Toolbar
|
Toolbar,
|
||||||
|
Button
|
||||||
} = require('@mui/material');
|
} = require('@mui/material');
|
||||||
const Footer = require('./components/Footer.js').default;
|
const Footer = require('./components/Footer.js').default;
|
||||||
const { Logo } = require('./components/header/index.js');
|
const { Logo } = require('./components/header/index.js');
|
||||||
@@ -216,59 +217,109 @@ class PrerenderProduct extends React.Component {
|
|||||||
},
|
},
|
||||||
cleanProductName(product.name)
|
cleanProductName(product.name)
|
||||||
),
|
),
|
||||||
// Manufacturer if available
|
// Manufacturer if available - exact match to SPA: only render Box if manufacturer exists
|
||||||
React.createElement(
|
product.manufacturer && React.createElement(
|
||||||
Box,
|
Box,
|
||||||
{ sx: { display: "flex", alignItems: "center", mb: 2 } },
|
{ sx: { display: "flex", alignItems: "center", mb: 2 } },
|
||||||
product.manufacturer && React.createElement(
|
React.createElement(
|
||||||
Typography,
|
Typography,
|
||||||
{ variant: 'body2', sx: { fontStyle: "italic" } },
|
{ variant: 'body2', sx: { fontStyle: "italic" } },
|
||||||
(this.props.t ? this.props.t('product.manufacturer') : 'Hersteller')+': '+product.manufacturer
|
(this.props.t ? this.props.t('product.manufacturer') : 'Hersteller')+': '+product.manufacturer
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
// Attribute images and chips with action buttons section
|
// Attribute images and chips with action buttons section - exact replica of SPA version
|
||||||
|
// SPA condition: (attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert]))
|
||||||
|
// This essentially means "if there are any attributes at all"
|
||||||
|
// For products with no attributes (like Vakuumbeutel), this section should NOT render
|
||||||
(attributes.length > 0) && React.createElement(
|
(attributes.length > 0) && React.createElement(
|
||||||
Box,
|
Box,
|
||||||
{ sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2, gap: 2 } },
|
{ sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2, gap: 2 } },
|
||||||
// Left side - attributes as chips (visible)
|
// Left side - attributes
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Stack,
|
Stack,
|
||||||
{ direction: 'row', spacing: 2, sx: { flexWrap: "wrap", gap: 1, flex: 1 } },
|
{ direction: 'row', spacing: 2, sx: { flexWrap: "wrap", gap: 1, flex: 1 } },
|
||||||
...attributes.map((attr, index) =>
|
// In prerender: attributes.filter(attribute => attributeImages[attribute.kMerkmalWert]) = [] (empty)
|
||||||
|
// Then: attributes.filter(attribute => !attributeImages[attribute.kMerkmalWert]) = all attributes as Chips
|
||||||
|
...attributes.map((attribute, index) =>
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Chip,
|
Chip,
|
||||||
{
|
{
|
||||||
key: index,
|
key: attribute.kMerkmalWert || index,
|
||||||
label: attr.cWert,
|
label: attribute.cWert,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
sx: { mb: 1 }
|
sx: { mb: 1 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
// Right side - invisible action buttons (space-filling only)
|
// Right side - action buttons (exact replica with invisible versions)
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Stack,
|
Stack,
|
||||||
{ direction: 'column', spacing: 1, sx: { flexShrink: 0 } },
|
{ direction: 'column', spacing: 1, sx: { flexShrink: 0 } },
|
||||||
// Invisible "Frage zum Artikel" button
|
// "Frage zum Artikel" button - exact replica but invisible
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Box,
|
Button,
|
||||||
{ sx: { height: "32px", width: "120px", visibility: "hidden" } }
|
{
|
||||||
|
variant: "outlined",
|
||||||
|
size: "small",
|
||||||
|
sx: {
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.5,
|
||||||
|
minWidth: "auto",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
visibility: "hidden",
|
||||||
|
pointerEvents: "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Frage zum Artikel"
|
||||||
),
|
),
|
||||||
// Invisible "Artikel Bewerten" button
|
// "Artikel Bewerten" button - exact replica but invisible
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Box,
|
Button,
|
||||||
{ sx: { height: "32px", width: "120px", visibility: "hidden" } }
|
{
|
||||||
|
variant: "outlined",
|
||||||
|
size: "small",
|
||||||
|
sx: {
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.5,
|
||||||
|
minWidth: "auto",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
visibility: "hidden",
|
||||||
|
pointerEvents: "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Artikel Bewerten"
|
||||||
),
|
),
|
||||||
// Invisible "Verfügbarkeit anfragen" button (conditional)
|
// "Verfügbarkeit anfragen" button - conditional, exact replica but invisible
|
||||||
(product.available !== 1 && product.availableSupplier !== 1) && React.createElement(
|
(product.available !== 1 && product.availableSupplier !== 1) && React.createElement(
|
||||||
Box,
|
Button,
|
||||||
{ sx: { height: "32px", width: "140px", visibility: "hidden" } }
|
{
|
||||||
|
variant: "outlined",
|
||||||
|
size: "small",
|
||||||
|
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"
|
||||||
|
},
|
||||||
|
visibility: "hidden",
|
||||||
|
pointerEvents: "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Verfügbarkeit anfragen"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
// Weight
|
// Weight
|
||||||
product.weight && product.weight > 0 && React.createElement(
|
(product.weight && product.weight > 0) ? React.createElement(
|
||||||
Box,
|
Box,
|
||||||
{ sx: { mb: 2 } },
|
{ sx: { mb: 2 } },
|
||||||
React.createElement(
|
React.createElement(
|
||||||
@@ -276,7 +327,7 @@ class PrerenderProduct extends React.Component {
|
|||||||
{ variant: 'body2', color: 'text.secondary' },
|
{ 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`)
|
(this.props.t ? this.props.t('product.weight', { weight: product.weight.toFixed(1).replace(".", ",") }) : `Gewicht: ${product.weight.toFixed(1).replace(".", ",")} kg`)
|
||||||
)
|
)
|
||||||
),
|
) : null,
|
||||||
// Price and availability section
|
// Price and availability section
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Box,
|
Box,
|
||||||
@@ -299,84 +350,100 @@ class PrerenderProduct extends React.Component {
|
|||||||
gap: 2,
|
gap: 2,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Left side - Price information
|
// Left side - Price information (exact match to SPA)
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Box,
|
Box,
|
||||||
{ sx: { flex: 1 } },
|
null,
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Typography,
|
Typography,
|
||||||
{
|
{
|
||||||
variant: "h4",
|
variant: "h4",
|
||||||
color: "primary",
|
color: "primary",
|
||||||
sx: { fontWeight: "bold", mb: 1 }
|
sx: { fontWeight: "bold" }
|
||||||
},
|
},
|
||||||
priceWithTax
|
priceWithTax
|
||||||
),
|
),
|
||||||
// VAT info with fixed height
|
// VAT info (exact match to SPA - direct Typography, no wrapper)
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Box,
|
Typography,
|
||||||
{ sx: { minHeight: "20px", mb: 1 } },
|
{ variant: 'body2', color: 'text.secondary' },
|
||||||
product.vat && React.createElement(
|
(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}` :
|
||||||
|
"")
|
||||||
|
),
|
||||||
|
// Shipping class (exact match to SPA - direct Typography, conditional render)
|
||||||
|
product.versandklasse &&
|
||||||
|
product.versandklasse != "standard" &&
|
||||||
|
product.versandklasse != "kostenlos" && React.createElement(
|
||||||
Typography,
|
Typography,
|
||||||
{ variant: 'body2', color: 'text.secondary' },
|
{ variant: 'body2', color: 'text.secondary' },
|
||||||
(this.props.t ? this.props.t('product.inclVat', { vat: product.vat }) : `inkl. ${product.vat}% MwSt.`)
|
product.versandklasse
|
||||||
)
|
)
|
||||||
),
|
|
||||||
// Shipping class with fixed height
|
|
||||||
React.createElement(
|
|
||||||
Box,
|
|
||||||
{ sx: { minHeight: "20px", mb: 1 } },
|
|
||||||
product.versandklasse &&
|
|
||||||
product.versandklasse != "standard" &&
|
|
||||||
product.versandklasse != "kostenlos" && React.createElement(
|
|
||||||
Typography,
|
|
||||||
{ variant: 'body2', color: 'text.secondary' },
|
|
||||||
product.versandklasse
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
// Right side - Cart button area with availability info
|
// Right side - Complex cart button area structure (matching SPA exactly)
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Box,
|
Box,
|
||||||
{
|
{
|
||||||
sx: {
|
sx: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: { xs: "column", sm: "row" },
|
||||||
minWidth: { xs: "100%", sm: "200px" }
|
gap: 2,
|
||||||
|
alignItems: "flex-start",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Reserved space for AddToCartButton (will be populated by SPA)
|
// Empty steckling column placeholder - maintains flex positioning
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ sx: { display: "flex", flexDirection: "column" } }
|
||||||
|
// Empty - no steckling for this product
|
||||||
|
),
|
||||||
|
// Main cart button column (exact match to SPA structure)
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Box,
|
Box,
|
||||||
{
|
{
|
||||||
sx: {
|
sx: {
|
||||||
minHeight: "48px",
|
|
||||||
mb: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
// Availability and delivery time info (matching ProductDetailPage)
|
|
||||||
React.createElement(
|
|
||||||
Typography,
|
|
||||||
{
|
|
||||||
variant: 'caption',
|
|
||||||
sx: {
|
|
||||||
fontStyle: "italic",
|
|
||||||
color: "text.secondary",
|
|
||||||
textAlign: "center",
|
|
||||||
minHeight: "28px",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
flexDirection: "column",
|
||||||
justifyContent: "center"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
product.id && product.id.toString().endsWith("steckling") ?
|
// AddToCartButton placeholder - invisible button that reserves exact space
|
||||||
(this.props.t ? this.props.t('delivery.times.cutting14Days') : "Lieferzeit: 14 Tage") :
|
React.createElement(
|
||||||
product.available == 1 ?
|
Button,
|
||||||
(this.props.t ? this.props.t('delivery.times.standard2to3Days') : "Lieferzeit: 2-3 Tage") :
|
{
|
||||||
product.availableSupplier == 1 ?
|
variant: "contained",
|
||||||
(this.props.t ? this.props.t('delivery.times.supplier7to9Days') : "Lieferzeit: 7-9 Tage") :
|
size: "large",
|
||||||
(product.available ? '✅ Verfügbar' : '❌ Nicht verfügbar')
|
sx: {
|
||||||
|
visibility: "hidden",
|
||||||
|
pointerEvents: "none",
|
||||||
|
height: "36px",
|
||||||
|
width: "140px",
|
||||||
|
minWidth: "140px",
|
||||||
|
maxWidth: "140px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"In den Warenkorb"
|
||||||
|
),
|
||||||
|
// Delivery time Typography (exact match to SPA)
|
||||||
|
React.createElement(
|
||||||
|
Typography,
|
||||||
|
{
|
||||||
|
variant: 'caption',
|
||||||
|
sx: {
|
||||||
|
fontStyle: "italic",
|
||||||
|
color: "text.secondary",
|
||||||
|
textAlign: "center",
|
||||||
|
mt: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
product.id && 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") :
|
||||||
|
""
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -109,21 +109,28 @@ class Images extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
<>
|
||||||
<CardMedia
|
<Box sx={{ position: 'relative', display: 'inline-block', width: '499px', maxWidth: '100%' }}>
|
||||||
component="img"
|
<CardMedia
|
||||||
height="400"
|
component="img"
|
||||||
image={getImagePath(this.props.pictureList)}
|
height="400"
|
||||||
sx={{
|
image={getImagePath(this.props.pictureList)}
|
||||||
objectFit: 'contain',
|
sx={{
|
||||||
cursor: 'pointer',
|
objectFit: 'contain',
|
||||||
transition: 'transform 0.2s ease-in-out',
|
cursor: 'pointer',
|
||||||
'&:hover': {
|
transition: 'transform 0.2s ease-in-out',
|
||||||
transform: 'scale(1.02)'
|
width: '499px',
|
||||||
}
|
maxWidth: '100%',
|
||||||
}}
|
'&:hover': {
|
||||||
/>
|
transform: 'scale(1.02)'
|
||||||
</Box>
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Stack direction="row" spacing={2} sx={{ justifyContent: 'flex-start', mt: 1, mb: 1 }}>
|
||||||
|
{/* Empty thumbnail gallery for prerender - reserves the mt+mb spacing (16px) */}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +138,7 @@ class Images extends Component {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.state.pics[this.state.mainPic] && (
|
{this.state.pics[this.state.mainPic] && (
|
||||||
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
<Box sx={{ position: 'relative', display: 'inline-block', width: '499px', maxWidth: '100%' }}>
|
||||||
<CardMedia
|
<CardMedia
|
||||||
component="img"
|
component="img"
|
||||||
height="400"
|
height="400"
|
||||||
@@ -139,6 +146,8 @@ class Images extends Component {
|
|||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'transform 0.2s ease-in-out',
|
transition: 'transform 0.2s ease-in-out',
|
||||||
|
width: '499px',
|
||||||
|
maxWidth: '100%',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
transform: 'scale(1.02)'
|
transform: 'scale(1.02)'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user