sheets
Some checks failed
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

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.

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,

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 || [],
}; };

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;

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>
); );

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>
); );
} }

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}>

View File

@@ -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>
); );

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>
); );

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,
}, },
}); });