sheets
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 17m33s

This commit is contained in:
2026-01-30 22:10:01 +01:00
parent 757df76f36
commit e4eabd7a86
56 changed files with 484 additions and 475 deletions
+210 -314
View File
@@ -21,234 +21,149 @@ Font.register({
// Industrial/technical/restrained design - STYLEGUIDE.md compliant // Industrial/technical/restrained design - STYLEGUIDE.md compliant
const styles = StyleSheet.create({ const styles = StyleSheet.create({
page: { page: {
// Large margins for engineering documentation feel. color: '#111827', // Text Primary
// Extra bottom padding reserves space for the fixed footer so content lineHeight: 1.5,
// (esp. long descriptions) doesn't render underneath it. backgroundColor: '#FFFFFF',
paddingTop: 72, paddingTop: 0,
paddingLeft: 72, paddingBottom: 100,
paddingRight: 72,
paddingBottom: 140,
fontFamily: 'Helvetica', fontFamily: 'Helvetica',
fontSize: 10,
color: '#1F2933', // Dark gray text
lineHeight: 1.5, // Generous line height
backgroundColor: '#F8F9FA', // Almost white background
}, },
// Engineering documentation header // Hero-style header
hero: {
backgroundColor: '#FFFFFF',
paddingTop: 24,
paddingBottom: 0,
paddingHorizontal: 72,
marginBottom: 20,
position: 'relative',
borderBottomWidth: 0,
borderBottomColor: '#e5e7eb',
},
header: { header: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'center',
marginBottom: 48, // Large spacing marginBottom: 16,
paddingBottom: 24,
borderBottom: '2px solid #E6E9ED', // Light gray separator
},
// Logo area - industrial style
logoArea: {
flexDirection: 'column',
alignItems: 'flex-start',
},
// Optional image logo container (keeps header height stable)
logoContainer: {
width: 120,
height: 32,
justifyContent: 'center',
},
// Image logo (preferred when available)
logo: {
width: 110,
height: 28,
objectFit: 'contain',
}, },
logoText: { logoText: {
fontSize: 20, fontSize: 24,
fontWeight: 700, fontWeight: 700,
color: '#0E2A47', // Dark navy color: '#000d26',
letterSpacing: 1, letterSpacing: 1,
textTransform: 'uppercase', textTransform: 'uppercase',
}, },
logoSubtext: {
fontSize: 10,
fontWeight: 400,
color: '#6B7280', // Medium gray
letterSpacing: 0.5,
marginTop: 2,
},
// Document info - technical style
docInfo: {
textAlign: 'right',
alignItems: 'flex-end',
},
docTitle: { docTitle: {
fontSize: 16, fontSize: 10,
fontWeight: 700, fontWeight: 700,
color: '#0E2A47', // Dark navy color: '#001a4d',
marginBottom: 8, letterSpacing: 2,
letterSpacing: 0.5,
textTransform: 'uppercase', textTransform: 'uppercase',
}, },
skuContainer: { productRow: {
backgroundColor: '#E6E9ED', // Light gray background flexDirection: 'row',
paddingHorizontal: 16, alignItems: 'center',
paddingVertical: 8, gap: 20,
border: '1px solid #E6E9ED', },
productInfoCol: {
flex: 1,
justifyContent: 'center',
},
productImageCol: {
flex: 1,
height: 120,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
borderWidth: 1,
borderColor: '#e5e7eb',
backgroundColor: '#FFFFFF',
overflow: 'hidden',
}, },
skuLabel: { // Product Hero Info
fontSize: 8, productHero: {
color: '#6B7280', // Medium gray marginTop: 0,
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 4,
},
skuValue: {
fontSize: 14,
fontWeight: 700,
color: '#0E2A47', // Dark navy
},
// Product section - technical specification style
productSection: {
marginBottom: 40,
backgroundColor: '#FFFFFF', // White background for content blocks
padding: 24,
border: '1px solid #E6E9ED',
}, },
productName: { productName: {
fontSize: 24, fontSize: 24,
fontWeight: 700, fontWeight: 700,
color: '#0E2A47', // Dark navy color: '#000d26',
marginBottom: 12, marginBottom: 0,
lineHeight: 1.2,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.5, letterSpacing: -0.5,
}, },
productMeta: { productMeta: {
fontSize: 12, fontSize: 10,
color: '#6B7280', // Medium gray color: '#4b5563',
fontWeight: 500, fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 1,
}, },
// Content sections - rectangular blocks heroImage: {
width: '100%',
height: '100%',
objectFit: 'contain',
},
noImage: {
fontSize: 8,
color: '#9ca3af',
textAlign: 'center',
},
// Content Area
content: {
paddingHorizontal: 72,
},
// Content sections
section: { section: {
marginBottom: 32, marginBottom: 20,
backgroundColor: '#FFFFFF',
padding: 24,
border: '1px solid #E6E9ED',
}, },
sectionTitle: { sectionTitle: {
fontSize: 14, fontSize: 14,
fontWeight: 700, fontWeight: 700,
color: '#0E2A47', // Dark navy color: '#000d26', // Primary Dark
marginBottom: 16, marginBottom: 8,
letterSpacing: 0.5,
textTransform: 'uppercase', textTransform: 'uppercase',
borderBottom: '1px solid #E6E9ED', letterSpacing: -0.2,
paddingBottom: 8, },
sectionAccent: {
width: 30,
height: 3,
backgroundColor: '#82ed20', // Accent Green
marginBottom: 8,
borderRadius: 1.5,
}, },
// Description - technical documentation style
description: { description: {
fontSize: 10, fontSize: 11,
lineHeight: 1.6, lineHeight: 1.7,
color: '#1F2933', // Dark gray text color: '#4b5563', // Text Secondary
marginBottom: 0,
}, },
// Cross-section table - engineering specification style // Technical data table
table: {
marginTop: 16,
borderWidth: 1,
borderColor: '#E6E9ED',
},
tableHeader: {
flexDirection: 'row',
backgroundColor: '#E6E9ED',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
},
tableHeaderCell: {
flex: 1,
padding: 8,
fontSize: 10,
fontWeight: 700,
color: '#0E2A47',
textTransform: 'uppercase',
letterSpacing: 0.3,
},
tableHeaderCellLast: {
borderRightWidth: 0,
},
tableHeaderCellWithDivider: {
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
},
tableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
},
tableCell: {
flex: 1,
padding: 8,
fontSize: 10,
color: '#1F2933',
},
tableCellLast: {
borderRightWidth: 0,
},
tableCellWithDivider: {
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
},
tableRowAlt: {
backgroundColor: '#F8F9FA',
},
// Specifications - technical data style
specsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
// Backwards-compatible alias used by the component markup
specsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
// Technical data table (used for the metagrid)
specsTable: { specsTable: {
borderWidth: 1, marginTop: 8,
borderColor: '#E6E9ED', border: '1px solid #e5e7eb',
borderRadius: 8,
overflow: 'hidden',
}, },
specsTableRow: { specsTableRow: {
flexDirection: 'row', flexDirection: 'row',
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#E6E9ED', borderBottomColor: '#e5e7eb',
}, },
specsTableRowLast: { specsTableRowLast: {
@@ -256,63 +171,35 @@ const styles = StyleSheet.create({
}, },
specsTableLabelCell: { specsTableLabelCell: {
flex: 3, flex: 1,
paddingVertical: 8, paddingVertical: 4,
paddingHorizontal: 8, paddingHorizontal: 16,
backgroundColor: '#F8F9FA', backgroundColor: '#f8f9fa',
borderRightWidth: 1, borderRightWidth: 1,
borderRightColor: '#E6E9ED', borderRightColor: '#e5e7eb',
justifyContent: 'center',
}, },
specsTableValueCell: { specsTableValueCell: {
flex: 4, flex: 1,
paddingVertical: 8, paddingVertical: 4,
paddingHorizontal: 8, paddingHorizontal: 16,
justifyContent: 'center',
}, },
specsTableLabelText: { specsTableLabelText: {
fontSize: 9, fontSize: 9,
fontWeight: 700, fontWeight: 700,
color: '#0E2A47', color: '#000d26',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.3, letterSpacing: 0.5,
lineHeight: 1.2,
}, },
specsTableValueText: { specsTableValueText: {
fontSize: 10, fontSize: 10,
color: '#1F2933', color: '#111827',
lineHeight: 1.4, fontWeight: 500,
}, },
specColumn: { // Categories
width: '48%',
marginRight: '4%',
marginBottom: 16,
},
specItem: {
marginBottom: 12,
},
specLabel: {
fontSize: 9,
fontWeight: 700,
color: '#0E2A47',
marginBottom: 4,
textTransform: 'uppercase',
letterSpacing: 0.3,
},
specValue: {
fontSize: 10,
color: '#1F2933',
lineHeight: 1.4,
},
// Categories - technical classification
categories: { categories: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
@@ -320,42 +207,48 @@ const styles = StyleSheet.create({
}, },
categoryTag: { categoryTag: {
backgroundColor: '#E6E9ED', backgroundColor: '#f8f9fa',
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 6, paddingVertical: 6,
border: '1px solid #E6E9ED', border: '1px solid #e5e7eb',
borderRadius: 100,
}, },
categoryText: { categoryText: {
fontSize: 9, fontSize: 8,
color: '#6B7280', color: '#4b5563',
fontWeight: 500, fontWeight: 700,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.3, letterSpacing: 0.5,
}, },
// Engineering documentation footer // Footer
footer: { footer: {
position: 'absolute', position: 'absolute',
bottom: 48, bottom: 40,
left: 72, left: 72,
right: 72, right: 72,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
paddingTop: 24, paddingTop: 24,
borderTop: '2px solid #E6E9ED', borderTop: '1px solid #e5e7eb',
fontSize: 9,
color: '#6B7280',
}, },
footerLeft: { footerText: {
fontSize: 8,
color: '#9ca3af',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: 1,
},
footerBrand: {
fontSize: 10,
fontWeight: 700, fontWeight: 700,
color: '#0E2A47', color: '#000d26',
}, textTransform: 'uppercase',
letterSpacing: 1,
footerRight: {
color: '#6B7280',
}, },
}); });
@@ -364,6 +257,7 @@ interface ProductData {
name: string; name: string;
shortDescriptionHtml: string; shortDescriptionHtml: string;
descriptionHtml: string; descriptionHtml: string;
applicationHtml?: string;
images: string[]; images: string[];
featuredImage: string | null; featuredImage: string | null;
sku: string; sku: string;
@@ -418,99 +312,101 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
return ( return (
<Document> <Document>
<Page size="A4" style={styles.page}> <Page size="A4" style={styles.page}>
{/* Clean, minimal header */} {/* Hero Header */}
<View style={styles.header}> <View style={styles.hero}>
<View style={styles.logoArea}> <View style={styles.header}>
<View style={styles.logoContainer}> <View>
{logoUrl ? ( <Text style={styles.logoText}>KLZ</Text>
/* eslint-disable-next-line jsx-a11y/alt-text */
<Image src={logoUrl} style={styles.logo} />
) : (
<View>
<Text style={styles.logoText}>KLZ</Text>
<Text style={styles.logoSubtext}>Cables</Text>
</View>
)}
</View> </View>
</View>
<View style={styles.docInfo}>
<Text style={styles.docTitle}> <Text style={styles.docTitle}>
{labels.productDatasheet} {labels.productDatasheet}
</Text> </Text>
<View style={styles.skuContainer}>
<Text style={styles.skuLabel}>{labels.sku}</Text>
<Text style={styles.skuValue}>{product.sku}</Text>
</View>
</View> </View>
</View>
{/* Product section - clean and prominent */} <View style={styles.productRow}>
<View style={styles.productSection}> <View style={styles.productInfoCol}>
<Text style={styles.productName}>{product.name}</Text> <View style={styles.productHero}>
<Text style={styles.productMeta}> <View style={styles.categories}>
{product.categories.map(cat => cat.name).join(' • ')} {product.categories.map((cat, index) => (
</Text> <Text key={index} style={styles.productMeta}>
</View> {cat.name}{index < product.categories.length - 1 ? ' • ' : ''}
{/* Description section */}
{(product.shortDescriptionHtml || product.descriptionHtml) && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.description}</Text>
<Text style={styles.description}>
{stripHtml(product.shortDescriptionHtml || product.descriptionHtml)}
</Text>
</View>
)}
{/* Technical specifications */}
{product.attributes && product.attributes.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
<View style={styles.specsTable}>
{product.attributes.map((attr, index) => (
<View
key={index}
style={[
styles.specsTableRow,
index === product.attributes.length - 1 &&
styles.specsTableRowLast,
]}
>
<View style={styles.specsTableLabelCell}>
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
</View>
<View style={styles.specsTableValueCell}>
<Text style={styles.specsTableValueText}>
{attr.options.join(', ')}
</Text> </Text>
</View> ))}
</View> </View>
))} <Text style={styles.productName}>{product.name}</Text>
</View>
</View>
<View style={styles.productImageCol}>
{product.featuredImage ? (
<Image src={product.featuredImage} style={styles.heroImage} />
) : (
<Text style={styles.noImage}>{labels.noImage}</Text>
)}
</View> </View>
</View> </View>
)} </View>
{/* Categories as clean tags */} <View style={styles.content}>
{product.categories && product.categories.length > 0 && ( {/* Description section */}
<View style={styles.section}> {(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && (
<Text style={styles.sectionTitle}>{labels.categories}</Text> <View style={styles.section}>
<View style={styles.categories}> <Text style={styles.sectionTitle}>{labels.description}</Text>
{product.categories.map((cat, index) => ( <View style={styles.sectionAccent} />
<View key={index} style={styles.categoryTag}> <Text style={styles.description}>
<Text style={styles.categoryText}>{cat.name}</Text> {stripHtml(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml)}
</View> </Text>
))}
</View> </View>
</View> )}
)}
{/* Technical specifications */}
{product.attributes && product.attributes.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
<View style={styles.sectionAccent} />
<View style={styles.specsTable}>
{product.attributes.map((attr, index) => (
<View
key={index}
style={[
styles.specsTableRow,
index === product.attributes.length - 1 &&
styles.specsTableRowLast,
]}
>
<View style={styles.specsTableLabelCell}>
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
</View>
<View style={styles.specsTableValueCell}>
<Text style={styles.specsTableValueText}>
{attr.options.join(', ')}
</Text>
</View>
</View>
))}
</View>
</View>
)}
{/* Categories as clean tags */}
{product.categories && product.categories.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.categories}</Text>
<View style={styles.sectionAccent} />
<View style={styles.categories}>
{product.categories.map((cat, index) => (
<View key={index} style={styles.categoryTag}>
<Text style={styles.categoryText}>{cat.name}</Text>
</View>
))}
</View>
</View>
)}
</View>
{/* Minimal footer */} {/* Minimal footer */}
<View style={styles.footer} fixed> <View style={styles.footer} fixed>
<Text style={styles.footerLeft}> <Text style={styles.footerBrand}>KLZ CABLES</Text>
{labels.sku}: {product.sku} <Text style={styles.footerText}>
</Text>
<Text style={styles.footerRight}>
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', { {new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4 -1
View File
@@ -39,6 +39,7 @@ type MdxProduct = {
categories: string[]; categories: string[];
images: string[]; images: string[];
descriptionHtml: string; descriptionHtml: string;
applicationHtml: string;
}; };
type MdxIndex = Map<string, MdxProduct>; // key: normalized designation/title type MdxIndex = Map<string, MdxProduct>; // key: normalized designation/title
@@ -85,9 +86,10 @@ function buildMdxIndex(locale: 'en' | 'de'): MdxIndex {
const images = Array.isArray(data.images) ? data.images.map((i: any) => normalizeValue(String(i))).filter(Boolean) : []; const images = Array.isArray(data.images) ? data.images.map((i: any) => normalizeValue(String(i))).filter(Boolean) : [];
const descriptionHtml = extractDescriptionFromMdxFrontmatter(data); const descriptionHtml = extractDescriptionFromMdxFrontmatter(data);
const applicationHtml = normalizeValue(String(data?.application || ''));
const slug = path.basename(file, '.mdx'); const slug = path.basename(file, '.mdx');
idx.set(normalizeExcelKey(title), { slug, title, sku, categories, images, descriptionHtml }); idx.set(normalizeExcelKey(title), { slug, title, sku, categories, images, descriptionHtml, applicationHtml });
} }
return idx; return idx;
@@ -183,6 +185,7 @@ async function loadProductsFromExcelAndMdx(locale: 'en' | 'de'): Promise<Product
name: title, name: title,
shortDescriptionHtml: '', shortDescriptionHtml: '',
descriptionHtml, descriptionHtml,
applicationHtml: mdx?.applicationHtml || '',
images: mdx?.images || [], images: mdx?.images || [],
featuredImage: (mdx?.images && mdx.images[0]) || null, featuredImage: (mdx?.images && mdx.images[0]) || null,
sku: mdx?.sku || title, sku: mdx?.sku || title,
+66 -17
View File
@@ -1129,7 +1129,7 @@ function buildMediumVoltageCrossSectionTableFromNewExcel(args: {
export function buildDatasheetModel(args: { product: ProductData; locale: 'en' | 'de' }): DatasheetModel { export function buildDatasheetModel(args: { product: ProductData; locale: 'en' | 'de' }): DatasheetModel {
const labels = getLabels(args.locale); const labels = getLabels(args.locale);
const categoriesLine = (args.product.categories || []).map(c => stripHtml(c.name)).join(' • '); const categoriesLine = (args.product.categories || []).map(c => stripHtml(c.name)).join(' • ');
const descriptionText = stripHtml(args.product.shortDescriptionHtml || args.product.descriptionHtml || ''); const descriptionText = stripHtml(args.product.applicationHtml || '');
const heroSrc = resolveMediaToLocalPath(args.product.featuredImage || args.product.images?.[0] || null); const heroSrc = resolveMediaToLocalPath(args.product.featuredImage || args.product.images?.[0] || null);
const productUrl = getProductUrl(args.product); const productUrl = getProductUrl(args.product);
@@ -1173,22 +1173,71 @@ export function buildDatasheetModel(args: { product: ProductData; locale: 'en' |
productUrl, productUrl,
}, },
labels, labels,
technicalItems: [ technicalItems: (() => {
...(excelModel.ok ? excelModel.technicalItems : []), if (!isMediumVoltageProduct(args.product)) {
...(isMediumVoltageProduct(args.product) return excelModel.ok ? excelModel.technicalItems : [];
? args.locale === 'de' }
? [
{ label: 'Prüfspannung 6/10 kV', value: '21 kV' }, const pn = normalizeDesignation(args.product.name || '');
{ label: 'Prüfspannung 12/20 kV', value: '42 kV' }, const isAl = /^NA/.test(pn);
{ label: 'Prüfspannung 18/30 kV', value: '63 kV' }, const isFL = pn.includes('FL');
] const isF = !isFL && pn.includes('F');
: [
{ label: 'Test voltage 6/10 kV', value: '21 kV' }, const findExcelVal = (labelPart: string) => {
{ label: 'Test voltage 12/20 kV', value: '42 kV' }, const found = excelModel.technicalItems.find(it => it.label.toLowerCase().includes(labelPart.toLowerCase()));
{ label: 'Test voltage 18/30 kV', value: '63 kV' }, return found ? found.value : null;
] };
: []),
], const items: KeyValueItem[] = [];
if (args.locale === 'de') {
items.push({ label: 'Leitermaterial', value: isAl ? 'Aluminium' : 'Kupfer' });
items.push({ label: 'Leiterklasse', value: isAl ? 'Klasse 1' : 'Klasse 2 mehrdrähtig' });
items.push({ label: 'Aderisolation', value: 'VPE DIX8' });
items.push({ label: 'Feldsteuerung', value: 'innere und äußere Leitschicht aus halbleitendem Kunststoff - 3-fach-extrudiert' });
items.push({ label: 'Schirm', value: 'Kupferdrähte + Querleitwendel' });
items.push({ label: 'Längswasserdichtigkeit', value: (isF || isFL) ? 'ja, mit Quellvliess' : 'nein' });
items.push({ label: 'Querwasserdichtigkeit', value: isFL ? 'ja, Al-Band' : 'nein' });
items.push({ label: 'Mantelmaterial', value: 'Polyethylen DMP2' });
items.push({ label: 'Mantelfarbe', value: 'schwarz' });
items.push({ label: 'Flammwidrigkeit', value: 'nein' });
items.push({ label: 'UV-beständig', value: 'ja' });
items.push({ label: 'Max. zulässige Leitertemperatur', value: findExcelVal('Leitertemperatur') || '90°C' });
items.push({ label: 'Zul. Kabelaußentemperatur, fest verlegt', value: findExcelVal('fest verlegt') || '70°C' });
items.push({ label: 'Zul. Kabelaußentemperatur, in Bewegung', value: findExcelVal('in Bewegung') || '-20 °C bis +70 °C' });
items.push({ label: 'Maximale Kurzschlußtemperatur', value: findExcelVal('Kurzschlußtemperatur') || '+250 °C' });
items.push({ label: 'Min. Biegeradius, fest verlegt', value: findExcelVal('Biegeradius') || '15 facher Durchmesser' });
items.push({ label: 'Mindesttemperatur Verlegung', value: findExcelVal('Verlegung') || '-5 °C' });
items.push({ label: 'Metermarkierung', value: 'ja' });
items.push({ label: 'Teilentladung', value: findExcelVal('Teilentladung') || '2 pC' });
items.push({ label: 'Prüfspannung 6/10 kV', value: '21 kV' });
items.push({ label: 'Prüfspannung 12/20 kV', value: '42 kV' });
items.push({ label: 'Prüfspannung 18/30 kV', value: '63 kV' });
} else {
items.push({ label: 'Conductor material', value: isAl ? 'Aluminum' : 'Copper' });
items.push({ label: 'Conductor class', value: isAl ? 'Class 1' : 'Class 2 stranded' });
items.push({ label: 'Core insulation', value: 'XLPE DIX8' });
items.push({ label: 'Field control', value: 'inner and outer semiconducting layer made of semiconducting plastic - 3-fold extruded' });
items.push({ label: 'Screen', value: 'copper wires + transverse conductive helix' });
items.push({ label: 'Longitudinal water tightness', value: (isF || isFL) ? 'yes, with swelling tape' : 'no' });
items.push({ label: 'Transverse water tightness', value: isFL ? 'yes, Al-tape' : 'no' });
items.push({ label: 'Sheath material', value: 'Polyethylene DMP2' });
items.push({ label: 'Sheath color', value: 'black' });
items.push({ label: 'Flame retardancy', value: 'no' });
items.push({ label: 'UV resistant', value: 'yes' });
items.push({ label: 'Max. permissible conductor temperature', value: findExcelVal('conductor temperature') || '90°C' });
items.push({ label: 'Permissible cable outer temperature, fixed', value: findExcelVal('fixed') || '70°C' });
items.push({ label: 'Permissible cable outer temperature, in motion', value: findExcelVal('in motion') || '-20 °C to +70 °C' });
items.push({ label: 'Maximum short-circuit temperature', value: findExcelVal('short-circuit temperature') || '+250 °C' });
items.push({ label: 'Min. bending radius, fixed', value: findExcelVal('bending radius') || '15 times diameter' });
items.push({ label: 'Minimum laying temperature', value: findExcelVal('laying temperature') || '-5 °C' });
items.push({ label: 'Meter marking', value: 'yes' });
items.push({ label: 'Partial discharge', value: findExcelVal('Partial discharge') || '2 pC' });
items.push({ label: 'Test voltage 6/10 kV', value: '21 kV' });
items.push({ label: 'Test voltage 12/20 kV', value: '42 kV' });
items.push({ label: 'Test voltage 18/30 kV', value: '63 kV' });
}
return items;
})(),
voltageTables, voltageTables,
legendItems: crossSectionModel.legendItems || [], legendItems: crossSectionModel.legendItems || [],
}; };
+1
View File
@@ -3,6 +3,7 @@ export interface ProductData {
name: string; name: string;
shortDescriptionHtml: string; shortDescriptionHtml: string;
descriptionHtml: string; descriptionHtml: string;
applicationHtml: string;
images: string[]; images: string[];
featuredImage: string | null; featuredImage: string | null;
sku: string; sku: string;
+45 -39
View File
@@ -26,56 +26,62 @@ export function DatasheetDocument(props: { model: DatasheetModel; assets: Assets
return ( return (
<Document> <Document>
<Page size="A4" style={styles.page}> <Page size="A4" style={styles.page}>
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} /> <View style={styles.hero}>
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} /> <Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} isHero={true} />
<Text style={styles.h1}>{model.product.name}</Text> <View style={styles.productRow}>
{model.product.categoriesLine ? <Text style={styles.subhead}>{model.product.categoriesLine}</Text> : null} <View style={styles.productInfoCol}>
<View style={styles.productHero}>
<View style={styles.heroBox}> {model.product.categoriesLine ? <Text style={styles.productMeta}>{model.product.categoriesLine}</Text> : null}
{assets.heroDataUrl ? ( <Text style={styles.productName}>{model.product.name}</Text>
<Image src={assets.heroDataUrl} style={styles.heroImage} /> </View>
) : ( </View>
<Text style={styles.noImage}>{model.labels.noImage}</Text> <View style={styles.productImageCol}>
)} {assets.heroDataUrl ? (
<Image src={assets.heroDataUrl} style={styles.heroImage} />
) : (
<Text style={styles.noImage}>{model.labels.noImage}</Text>
)}
</View>
</View>
</View> </View>
{model.product.descriptionText ? ( <Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
<Section title={model.labels.description} minPresenceAhead={24}>
<Text style={styles.body}>{model.product.descriptionText}</Text>
</Section>
) : null}
{model.technicalItems.length ? ( <View style={styles.content}>
<Section title={model.labels.technicalData} minPresenceAhead={24}> {model.product.descriptionText ? (
<KeyValueGrid items={model.technicalItems} /> <Section title={model.labels.description} minPresenceAhead={24}>
</Section> <Text style={styles.body}>{model.product.descriptionText}</Text>
) : null} </Section>
) : null}
{model.technicalItems.length ? (
<Section title={model.labels.technicalData} minPresenceAhead={24}>
<KeyValueGrid items={model.technicalItems} />
</Section>
) : null}
</View>
</Page> </Page>
{/*
Render all voltage sections in a single flow so React-PDF can paginate naturally.
This avoids hard page breaks that waste remaining whitespace at the bottom of a page.
Each table section has break={false} to prevent breaking within individual tables,
but the overall flow allows tables to move to the next page if needed.
*/}
<Page size="A4" style={styles.page}> <Page size="A4" style={styles.page}>
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} /> <Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} />
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} /> <Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
{model.voltageTables.map((t: DatasheetVoltageTable) => ( <View style={styles.content}>
<View key={t.voltageLabel} style={{ marginBottom: 14 }} break={false} minPresenceAhead={24}> {model.voltageTables.map((t: DatasheetVoltageTable) => (
<Text style={styles.sectionTitle}>{`${model.labels.crossSection} — ${t.voltageLabel}`}</Text> <View key={t.voltageLabel} style={{ marginBottom: 14 }} break={false} minPresenceAhead={24}>
<Text style={styles.sectionTitle}>{`${model.labels.crossSection} — ${t.voltageLabel}`}</Text>
<View style={styles.sectionAccent} />
<DenseTable table={{ columns: t.columns, rows: t.rows }} firstColLabel={firstColLabel} />
</View>
))}
<DenseTable table={{ columns: t.columns, rows: t.rows }} firstColLabel={firstColLabel} /> {model.legendItems.length ? (
</View> <Section title={model.locale === 'de' ? 'ABKÜRZUNGEN' : 'ABBREVIATIONS'} minPresenceAhead={24}>
))} <KeyValueGrid items={model.legendItems} />
</Section>
{model.legendItems.length ? ( ) : null}
<Section title={model.locale === 'de' ? 'ABKÜRZUNGEN' : 'ABBREVIATIONS'} minPresenceAhead={24}> </View>
<KeyValueGrid items={model.legendItems} />
</Section>
) : null}
</Page> </Page>
</Document> </Document>
); );
+3 -3
View File
@@ -14,9 +14,9 @@ export function Footer(props: { locale: 'en' | 'de'; siteUrl?: string }): React.
return ( return (
<View style={styles.footer} fixed> <View style={styles.footer} fixed>
<Text>{siteUrl}</Text> <Text style={styles.footerBrand}>KLZ CABLES</Text>
<Text>{date}</Text> <Text style={styles.footerText}>{date}</Text>
<Text render={({ pageNumber, totalPages }) => `${pageNumber}/${totalPages}`} /> <Text style={styles.footerText} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} />
</View> </View>
); );
} }
+5 -6
View File
@@ -3,17 +3,16 @@ import { Image, Text, View } from '@react-pdf/renderer';
import { styles } from '../styles'; import { styles } from '../styles';
export function Header(props: { title: string; logoDataUrl?: string | null; qrDataUrl?: string | null }): React.ReactElement { export function Header(props: { title: string; logoDataUrl?: string | null; qrDataUrl?: string | null; isHero?: boolean }): React.ReactElement {
const { isHero = false } = props;
return ( return (
<View style={styles.header} fixed> <View style={isHero ? styles.header : [styles.header, { paddingHorizontal: 0, backgroundColor: 'transparent', borderBottomWidth: 0, marginBottom: 24, paddingTop: 40 }]}>
<View style={styles.headerLeft}> <View style={styles.headerLeft}>
{props.logoDataUrl ? ( {props.logoDataUrl ? (
<Image src={props.logoDataUrl} style={styles.logo} /> <Image src={props.logoDataUrl} style={styles.logo} />
) : ( ) : (
<View style={styles.brandFallback}> <Text style={styles.brandFallback}>KLZ</Text>
<Text style={styles.brandFallbackKlz}>KLZ</Text>
<Text style={styles.brandFallbackCables}>Cables</Text>
</View>
)} )}
</View> </View>
<View style={styles.headerRight}> <View style={styles.headerRight}>
@@ -8,37 +8,25 @@ export function KeyValueGrid(props: { items: KeyValueItem[] }): React.ReactEleme
const items = (props.items || []).filter(i => i.label && i.value); const items = (props.items || []).filter(i => i.label && i.value);
if (!items.length) return null; if (!items.length) return null;
// 4-column layout: (label, value, label, value) // 2-column layout: (label, value)
const rows: Array<[KeyValueItem, KeyValueItem | null]> = [];
for (let i = 0; i < items.length; i += 2) {
rows.push([items[i], items[i + 1] || null]);
}
return ( return (
<View style={styles.kvGrid}> <View style={styles.kvGrid}>
{rows.map(([left, right], rowIndex) => { {items.map((item, rowIndex) => {
const isLast = rowIndex === rows.length - 1; const isLast = rowIndex === items.length - 1;
const leftValue = left.unit ? `${left.value} ${left.unit}` : left.value; const value = item.unit ? `${item.value} ${item.unit}` : item.value;
const rightValue = right ? (right.unit ? `${right.value} ${right.unit}` : right.value) : '';
return ( return (
<View <View
key={`${left.label}-${rowIndex}`} key={`${item.label}-${rowIndex}`}
style={[styles.kvRow, rowIndex % 2 === 0 ? styles.kvRowAlt : null, isLast ? styles.kvRowLast : null]} style={[styles.kvRow, rowIndex % 2 === 0 ? styles.kvRowAlt : null, isLast ? styles.kvRowLast : null]}
wrap={false} wrap={false}
minPresenceAhead={12} minPresenceAhead={12}
> >
<View style={[styles.kvCell, { width: '18%' }]}> <View style={[styles.kvCell, { width: '50%' }]}>
<Text style={styles.kvLabelText}>{left.label}</Text> <Text style={styles.kvLabelText}>{item.label}</Text>
</View> </View>
<View style={[styles.kvCell, styles.kvMidDivider, { width: '32%' }]}> <View style={[styles.kvCell, { width: '50%' }]}>
<Text style={styles.kvValueText}>{leftValue}</Text> <Text style={styles.kvValueText}>{value}</Text>
</View>
<View style={[styles.kvCell, { width: '18%' }]}>
<Text style={styles.kvLabelText}>{right?.label || ''}</Text>
</View>
<View style={[styles.kvCell, { width: '32%' }]}>
<Text style={styles.kvValueText}>{rightValue}</Text>
</View> </View>
</View> </View>
); );
+2 -1
View File
@@ -11,8 +11,9 @@ export function Section(props: {
}): React.ReactElement { }): React.ReactElement {
const boxed = props.boxed ?? true; const boxed = props.boxed ?? true;
return ( return (
<View style={boxed ? styles.section : styles.sectionPlain} minPresenceAhead={props.minPresenceAhead}> <View style={styles.section} minPresenceAhead={props.minPresenceAhead}>
<Text style={styles.sectionTitle}>{props.title}</Text> <Text style={styles.sectionTitle}>{props.title}</Text>
<View style={styles.sectionAccent} />
{props.children} {props.children}
</View> </View>
); );
+138 -72
View File
@@ -5,146 +5,212 @@ import { Font, StyleSheet } from '@react-pdf/renderer';
Font.registerHyphenationCallback(word => [word]); Font.registerHyphenationCallback(word => [word]);
export const COLORS = { export const COLORS = {
navy: '#0E2A47', primary: '#001a4d',
mediumGray: '#6B7280', primaryDark: '#000d26',
darkGray: '#1F2933', accent: '#82ed20',
lightGray: '#E6E9ED', textPrimary: '#111827',
almostWhite: '#F8F9FA', textSecondary: '#4b5563',
headerBg: '#F6F8FB', textLight: '#9ca3af',
neutral: '#f8f9fa',
border: '#e5e7eb',
} as const; } as const;
export const styles = StyleSheet.create({ export const styles = StyleSheet.create({
page: { page: {
paddingTop: 54, paddingTop: 0,
paddingLeft: 54, paddingLeft: 30,
paddingRight: 54, paddingRight: 30,
paddingBottom: 72, paddingBottom: 60,
fontFamily: 'Helvetica', fontFamily: 'Helvetica',
fontSize: 10, fontSize: 10,
color: COLORS.darkGray, color: COLORS.textPrimary,
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
}, },
// Hero-style header
hero: {
backgroundColor: '#FFFFFF',
paddingTop: 30,
paddingBottom: 0,
paddingHorizontal: 0,
marginBottom: 20,
position: 'relative',
borderBottomWidth: 0,
borderBottomColor: COLORS.border,
},
header: { header: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
paddingVertical: 12, marginBottom: 24,
paddingHorizontal: 16, paddingHorizontal: 0,
backgroundColor: COLORS.headerBg,
borderBottomWidth: 1,
borderBottomColor: COLORS.lightGray,
marginBottom: 16,
}, },
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 }, headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
logo: { width: 110, height: 24, objectFit: 'contain' }, logo: { width: 100, height: 22, objectFit: 'contain' },
brandFallback: { flexDirection: 'row', alignItems: 'baseline', gap: 6 }, brandFallback: { fontSize: 20, fontWeight: 700, color: COLORS.primaryDark, letterSpacing: 1, textTransform: 'uppercase' },
brandFallbackKlz: { fontSize: 18, fontWeight: 700, color: COLORS.navy },
brandFallbackCables: { fontSize: 10, color: COLORS.mediumGray },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 10 }, headerRight: { flexDirection: 'row', alignItems: 'center', gap: 10 },
headerTitle: { fontSize: 9, fontWeight: 700, color: COLORS.navy, letterSpacing: 0.2 }, headerTitle: { fontSize: 9, fontWeight: 700, color: COLORS.primary, letterSpacing: 1.5, textTransform: 'uppercase' },
qr: { width: 34, height: 34, objectFit: 'contain' }, qr: { width: 30, height: 30, objectFit: 'contain' },
productRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 20,
},
productInfoCol: {
flex: 1,
justifyContent: 'center',
},
productImageCol: {
flex: 1,
height: 120,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
borderWidth: 1,
borderColor: COLORS.border,
backgroundColor: '#FFFFFF',
overflow: 'hidden',
},
productHero: {
marginTop: 0,
paddingHorizontal: 0,
},
productName: {
fontSize: 24,
fontWeight: 700,
color: COLORS.primaryDark,
marginBottom: 0,
textTransform: 'uppercase',
letterSpacing: -0.5,
},
productMeta: {
fontSize: 9,
color: COLORS.textSecondary,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 1,
marginBottom: 4,
},
content: {
paddingHorizontal: 0,
},
footer: { footer: {
position: 'absolute', position: 'absolute',
left: 54, left: 30,
right: 54, right: 30,
bottom: 36, bottom: 30,
paddingTop: 10, paddingTop: 16,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: COLORS.lightGray, borderTopColor: COLORS.border,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
fontSize: 8, alignItems: 'center',
color: COLORS.mediumGray,
}, },
footerBrand: { fontSize: 9, fontWeight: 700, color: COLORS.primaryDark, textTransform: 'uppercase', letterSpacing: 1 },
footerText: { fontSize: 8, color: COLORS.textLight, fontWeight: 500, textTransform: 'uppercase', letterSpacing: 0.5 },
h1: { fontSize: 18, fontWeight: 700, color: COLORS.navy, marginBottom: 6 }, h1: { fontSize: 22, fontWeight: 700, color: COLORS.primaryDark, marginBottom: 8, textTransform: 'uppercase' },
subhead: { fontSize: 10.5, color: COLORS.mediumGray, marginBottom: 14 }, subhead: { fontSize: 10, fontWeight: 700, color: COLORS.textSecondary, marginBottom: 16, textTransform: 'uppercase', letterSpacing: 0.5 },
heroBox: { heroBox: {
height: 110, height: 180,
borderRadius: 12,
borderWidth: 1, borderWidth: 1,
borderColor: COLORS.lightGray, borderColor: COLORS.border,
backgroundColor: COLORS.almostWhite, backgroundColor: '#FFFFFF',
marginBottom: 16, marginBottom: 24,
justifyContent: 'center', justifyContent: 'center',
overflow: 'hidden', overflow: 'hidden',
padding: 0,
}, },
heroImage: { width: '100%', height: '100%', objectFit: 'contain' }, heroImage: { width: '100%', height: '100%', objectFit: 'contain' },
noImage: { fontSize: 8, color: COLORS.mediumGray, paddingHorizontal: 12 }, noImage: { fontSize: 8, color: COLORS.textLight, textAlign: 'center' },
section: { section: {
borderWidth: 1, marginBottom: 10,
borderColor: COLORS.lightGray,
padding: 14,
marginBottom: 14,
},
sectionPlain: {
paddingVertical: 2,
marginBottom: 12,
}, },
sectionTitle: { sectionTitle: {
fontSize: 10, fontSize: 14,
fontWeight: 700, fontWeight: 700,
color: COLORS.navy, color: COLORS.primaryDark,
marginBottom: 8, marginBottom: 8,
letterSpacing: 0.2, textTransform: 'uppercase',
letterSpacing: -0.2,
}, },
body: { fontSize: 10, lineHeight: 1.5, color: COLORS.darkGray }, sectionAccent: {
width: 30,
height: 3,
backgroundColor: COLORS.accent,
marginBottom: 8,
borderRadius: 1.5,
},
body: { fontSize: 10, lineHeight: 1.6, color: COLORS.textSecondary },
kvGrid: { kvGrid: {
width: '100%', width: '100%',
borderWidth: 1, borderWidth: 1,
borderColor: COLORS.lightGray, borderColor: COLORS.border,
borderRadius: 8,
overflow: 'hidden',
}, },
kvRow: { kvRow: {
flexDirection: 'row', flexDirection: 'row',
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: COLORS.lightGray, borderBottomColor: COLORS.border,
}, },
kvRowAlt: { backgroundColor: COLORS.almostWhite }, kvRowAlt: { backgroundColor: COLORS.neutral },
kvRowLast: { borderBottomWidth: 0 }, kvRowLast: { borderBottomWidth: 0 },
kvCell: { paddingVertical: 6, paddingHorizontal: 8 }, kvCell: { paddingVertical: 3, paddingHorizontal: 12 },
// Visual separator between (label,value) pairs in the 4-col KV grid.
// Matches the engineering-table look and improves scanability.
kvMidDivider: { kvMidDivider: {
borderRightWidth: 1, borderRightWidth: 1,
borderRightColor: COLORS.lightGray, borderRightColor: COLORS.border,
}, },
kvLabelText: { fontSize: 8.5, fontWeight: 700, color: COLORS.mediumGray }, kvLabelText: { fontSize: 8, fontWeight: 700, color: COLORS.primaryDark, textTransform: 'uppercase', letterSpacing: 0.3 },
kvValueText: { fontSize: 9.5, color: COLORS.darkGray }, kvValueText: { fontSize: 9, color: COLORS.textPrimary, fontWeight: 500 },
tableWrap: { width: '100%', borderWidth: 1, borderColor: COLORS.lightGray, marginBottom: 14 }, tableWrap: {
width: '100%',
borderWidth: 1,
borderColor: COLORS.border,
borderRadius: 8,
overflow: 'hidden',
marginBottom: 16,
},
tableHeader: { tableHeader: {
width: '100%', width: '100%',
flexDirection: 'row', flexDirection: 'row',
backgroundColor: '#FFFFFF', backgroundColor: COLORS.neutral,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: COLORS.lightGray, borderBottomColor: COLORS.border,
}, },
tableHeaderCell: { tableHeaderCell: {
paddingVertical: 5, paddingVertical: 8,
paddingHorizontal: 4, paddingHorizontal: 6,
fontSize: 6.6, fontSize: 7,
fontWeight: 700, fontWeight: 700,
color: COLORS.navy, color: COLORS.primaryDark,
textTransform: 'uppercase',
letterSpacing: 0.2,
}, },
tableHeaderCellCfg: { tableHeaderCellCfg: {
paddingHorizontal: 6, paddingHorizontal: 8,
}, },
tableHeaderCellDivider: { tableHeaderCellDivider: {
borderRightWidth: 1, borderRightWidth: 1,
borderRightColor: COLORS.lightGray, borderRightColor: COLORS.border,
}, },
tableRow: { width: '100%', flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.lightGray }, tableRow: { width: '100%', flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.border },
tableRowAlt: { backgroundColor: COLORS.almostWhite }, tableRowAlt: { backgroundColor: '#FFFFFF' },
tableCell: { paddingVertical: 4, paddingHorizontal: 4, fontSize: 6.6, color: COLORS.darkGray }, tableCell: { paddingVertical: 6, paddingHorizontal: 6, fontSize: 7, color: COLORS.textSecondary, fontWeight: 500 },
tableCellCfg: { tableCellCfg: {
paddingHorizontal: 6, paddingHorizontal: 8,
}, },
tableCellDivider: { tableCellDivider: {
borderRightWidth: 1, borderRightWidth: 1,
borderRightColor: COLORS.lightGray, borderRightColor: COLORS.border,
}, },
}); });