feat: add prerender-single-product script for single product rendering with i18n support
This commit is contained in:
@@ -9,7 +9,8 @@ const {
|
||||
Chip,
|
||||
Stack,
|
||||
AppBar,
|
||||
Toolbar
|
||||
Toolbar,
|
||||
Button
|
||||
} = require('@mui/material');
|
||||
const Footer = require('./components/Footer.js').default;
|
||||
const { Logo } = require('./components/header/index.js');
|
||||
@@ -216,59 +217,109 @@ class PrerenderProduct extends React.Component {
|
||||
},
|
||||
cleanProductName(product.name)
|
||||
),
|
||||
// Manufacturer if available
|
||||
React.createElement(
|
||||
// Manufacturer if available - exact match to SPA: only render Box if manufacturer exists
|
||||
product.manufacturer && React.createElement(
|
||||
Box,
|
||||
{ sx: { display: "flex", alignItems: "center", mb: 2 } },
|
||||
product.manufacturer && React.createElement(
|
||||
React.createElement(
|
||||
Typography,
|
||||
{ variant: 'body2', sx: { fontStyle: "italic" } },
|
||||
(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(
|
||||
Box,
|
||||
{ sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2, gap: 2 } },
|
||||
// Left side - attributes as chips (visible)
|
||||
// Left side - attributes
|
||||
React.createElement(
|
||||
Stack,
|
||||
{ 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(
|
||||
Chip,
|
||||
{
|
||||
key: index,
|
||||
label: attr.cWert,
|
||||
{
|
||||
key: attribute.kMerkmalWert || index,
|
||||
label: attribute.cWert,
|
||||
disabled: true,
|
||||
sx: { mb: 1 }
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
// Right side - invisible action buttons (space-filling only)
|
||||
// Right side - action buttons (exact replica with invisible versions)
|
||||
React.createElement(
|
||||
Stack,
|
||||
{ direction: 'column', spacing: 1, sx: { flexShrink: 0 } },
|
||||
// Invisible "Frage zum Artikel" button
|
||||
// "Frage zum Artikel" button - exact replica but invisible
|
||||
React.createElement(
|
||||
Box,
|
||||
{ sx: { height: "32px", width: "120px", visibility: "hidden" } }
|
||||
Button,
|
||||
{
|
||||
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(
|
||||
Box,
|
||||
{ sx: { height: "32px", width: "120px", visibility: "hidden" } }
|
||||
Button,
|
||||
{
|
||||
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(
|
||||
Box,
|
||||
{ sx: { height: "32px", width: "140px", visibility: "hidden" } }
|
||||
Button,
|
||||
{
|
||||
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
|
||||
product.weight && product.weight > 0 && React.createElement(
|
||||
(product.weight && product.weight > 0) ? React.createElement(
|
||||
Box,
|
||||
{ sx: { mb: 2 } },
|
||||
React.createElement(
|
||||
@@ -276,7 +327,7 @@ class PrerenderProduct extends React.Component {
|
||||
{ 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`)
|
||||
)
|
||||
),
|
||||
) : null,
|
||||
// Price and availability section
|
||||
React.createElement(
|
||||
Box,
|
||||
@@ -299,84 +350,100 @@ class PrerenderProduct extends React.Component {
|
||||
gap: 2,
|
||||
}
|
||||
},
|
||||
// Left side - Price information
|
||||
// Left side - Price information (exact match to SPA)
|
||||
React.createElement(
|
||||
Box,
|
||||
{ sx: { flex: 1 } },
|
||||
null,
|
||||
React.createElement(
|
||||
Typography,
|
||||
{
|
||||
variant: "h4",
|
||||
color: "primary",
|
||||
sx: { fontWeight: "bold", mb: 1 }
|
||||
sx: { fontWeight: "bold" }
|
||||
},
|
||||
priceWithTax
|
||||
),
|
||||
// VAT info with fixed height
|
||||
// VAT info (exact match to SPA - direct Typography, no wrapper)
|
||||
React.createElement(
|
||||
Box,
|
||||
{ sx: { minHeight: "20px", mb: 1 } },
|
||||
product.vat && React.createElement(
|
||||
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}` :
|
||||
"")
|
||||
),
|
||||
// Shipping class (exact match to SPA - direct Typography, conditional render)
|
||||
product.versandklasse &&
|
||||
product.versandklasse != "standard" &&
|
||||
product.versandklasse != "kostenlos" && React.createElement(
|
||||
Typography,
|
||||
{ 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(
|
||||
Box,
|
||||
{
|
||||
sx: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minWidth: { xs: "100%", sm: "200px" }
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
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(
|
||||
Box,
|
||||
{
|
||||
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",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
flexDirection: "column",
|
||||
}
|
||||
},
|
||||
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") :
|
||||
(product.available ? '✅ Verfügbar' : '❌ Nicht verfügbar')
|
||||
// AddToCartButton placeholder - invisible button that reserves exact space
|
||||
React.createElement(
|
||||
Button,
|
||||
{
|
||||
variant: "contained",
|
||||
size: "large",
|
||||
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 (
|
||||
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="400"
|
||||
image={getImagePath(this.props.pictureList)}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.02)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<>
|
||||
<Box sx={{ position: 'relative', display: 'inline-block', width: '499px', maxWidth: '100%' }}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="400"
|
||||
image={getImagePath(this.props.pictureList)}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.2s ease-in-out',
|
||||
width: '499px',
|
||||
maxWidth: '100%',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.02)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</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 (
|
||||
<>
|
||||
{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
|
||||
component="img"
|
||||
height="400"
|
||||
@@ -139,6 +146,8 @@ class Images extends Component {
|
||||
objectFit: 'contain',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.2s ease-in-out',
|
||||
width: '499px',
|
||||
maxWidth: '100%',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.02)'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user