fix: restore complex table components in lib/pdf-datasheet.tsx
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 2m16s
Build & Deploy / 🏗️ Build (push) Successful in 3m41s
Build & Deploy / 🚀 Deploy (push) Successful in 1m14s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 4m25s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 2m16s
Build & Deploy / 🏗️ Build (push) Successful in 3m41s
Build & Deploy / 🚀 Deploy (push) Successful in 1m14s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 4m25s
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
@@ -1,16 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { Document, Page, View, Text, Image, StyleSheet, Font } from '@react-pdf/renderer';
|
||||
import type { DatasheetVoltageTable, KeyValueItem } from '../scripts/pdf/model/types';
|
||||
|
||||
// Register fonts (using system fonts for now, can be customized)
|
||||
Font.register({
|
||||
family: 'Helvetica',
|
||||
fonts: [
|
||||
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
|
||||
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
|
||||
],
|
||||
});
|
||||
// Standard built-in fonts are used.
|
||||
Font.registerHyphenationCallback((word) => [word]);
|
||||
|
||||
// ─── Brand Tokens (matching brochure) ────────────────────────────────────────
|
||||
const C = {
|
||||
navy: '#001a4d',
|
||||
navyDeep: '#000d26',
|
||||
@@ -30,32 +24,20 @@ const MARGIN = 56;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
color: C.gray900,
|
||||
lineHeight: 1.5,
|
||||
backgroundColor: C.white,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 80,
|
||||
fontFamily: 'Helvetica',
|
||||
},
|
||||
|
||||
// Hero-style header
|
||||
hero: {
|
||||
backgroundColor: C.white,
|
||||
paddingTop: 24,
|
||||
paddingBottom: 0,
|
||||
paddingHorizontal: MARGIN,
|
||||
marginBottom: 20,
|
||||
position: 'relative',
|
||||
borderBottomWidth: 0,
|
||||
paddingBottom: 80,
|
||||
paddingTop: 40,
|
||||
fontFamily: 'Helvetica',
|
||||
backgroundColor: C.white,
|
||||
color: C.gray900,
|
||||
},
|
||||
|
||||
hero: { paddingBottom: 20, marginBottom: 10 },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
|
||||
logoText: {
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
@@ -63,7 +45,6 @@ const styles = StyleSheet.create({
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
|
||||
docTitle: {
|
||||
fontSize: 8,
|
||||
fontWeight: 700,
|
||||
@@ -71,16 +52,8 @@ const styles = StyleSheet.create({
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
|
||||
productRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 20,
|
||||
},
|
||||
productInfoCol: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
productRow: { flexDirection: 'row', alignItems: 'center', gap: 20 },
|
||||
productInfoCol: { flex: 1, justifyContent: 'center' },
|
||||
productImageCol: {
|
||||
flex: 1,
|
||||
height: 120,
|
||||
@@ -92,51 +65,26 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: C.white,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
// Product Hero Info
|
||||
productHero: {
|
||||
marginTop: 0,
|
||||
},
|
||||
|
||||
productName: {
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
marginBottom: 0,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
|
||||
productMeta: {
|
||||
fontSize: 10,
|
||||
color: C.gray600,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
marginBottom: 2,
|
||||
},
|
||||
heroImage: { width: '100%', height: '100%', objectFit: 'contain' },
|
||||
noImage: { fontSize: 8, color: C.gray400, textAlign: 'center' },
|
||||
|
||||
heroImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
},
|
||||
|
||||
noImage: {
|
||||
fontSize: 8,
|
||||
color: C.gray400,
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
// Content Area
|
||||
content: {
|
||||
paddingHorizontal: MARGIN,
|
||||
},
|
||||
|
||||
// Content sections
|
||||
section: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
|
||||
content: {},
|
||||
section: { marginBottom: 20 },
|
||||
sectionTitle: {
|
||||
fontSize: 8,
|
||||
fontWeight: 700,
|
||||
@@ -145,7 +93,6 @@ const styles = StyleSheet.create({
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1.5,
|
||||
},
|
||||
|
||||
sectionAccent: {
|
||||
width: 30,
|
||||
height: 2,
|
||||
@@ -153,67 +100,9 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 8,
|
||||
borderRadius: 1,
|
||||
},
|
||||
description: { fontSize: 10, lineHeight: 1.7, color: C.gray600 },
|
||||
|
||||
description: {
|
||||
fontSize: 10,
|
||||
lineHeight: 1.7,
|
||||
color: C.gray600,
|
||||
},
|
||||
|
||||
// Technical data table
|
||||
specsTable: {
|
||||
marginTop: 4,
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
specsTableRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: C.gray200,
|
||||
},
|
||||
|
||||
specsTableRowLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
|
||||
specsTableLabelCell: {
|
||||
flex: 1,
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: C.offWhite,
|
||||
borderRightWidth: 0.5,
|
||||
borderRightColor: C.gray200,
|
||||
},
|
||||
|
||||
specsTableValueCell: {
|
||||
flex: 1,
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
|
||||
specsTableLabelText: {
|
||||
fontSize: 8,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
|
||||
specsTableValueText: {
|
||||
fontSize: 9,
|
||||
color: C.gray900,
|
||||
fontWeight: 400,
|
||||
},
|
||||
|
||||
// Categories
|
||||
categories: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 6,
|
||||
},
|
||||
|
||||
categories: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 },
|
||||
categoryTag: {
|
||||
backgroundColor: C.offWhite,
|
||||
paddingHorizontal: 10,
|
||||
@@ -222,7 +111,6 @@ const styles = StyleSheet.create({
|
||||
borderColor: C.gray200,
|
||||
borderRadius: 3,
|
||||
},
|
||||
|
||||
categoryText: {
|
||||
fontSize: 7,
|
||||
color: C.gray600,
|
||||
@@ -231,7 +119,6 @@ const styles = StyleSheet.create({
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
|
||||
// Footer — matches brochure style
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 28,
|
||||
@@ -244,7 +131,6 @@ const styles = StyleSheet.create({
|
||||
borderTopWidth: 2,
|
||||
borderTopColor: C.green,
|
||||
},
|
||||
|
||||
footerText: {
|
||||
fontSize: 7,
|
||||
color: C.gray400,
|
||||
@@ -252,7 +138,6 @@ const styles = StyleSheet.create({
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
|
||||
footerBrand: {
|
||||
fontSize: 9,
|
||||
fontWeight: 700,
|
||||
@@ -260,6 +145,43 @@ const styles = StyleSheet.create({
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1.5,
|
||||
},
|
||||
|
||||
kvGrid: { width: '100%', borderWidth: 1, borderColor: C.gray200 },
|
||||
kvRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: C.gray200 },
|
||||
kvRowAlt: { backgroundColor: C.offWhite },
|
||||
kvRowLast: { borderBottomWidth: 0 },
|
||||
kvCell: { paddingVertical: 6, paddingHorizontal: 8 },
|
||||
kvMidDivider: { borderRightWidth: 1, borderRightColor: C.gray200 },
|
||||
kvLabelText: { fontSize: 8.5, fontWeight: 700, color: C.gray600 },
|
||||
kvValueText: { fontSize: 9.5, color: C.gray900 },
|
||||
|
||||
tableWrap: { width: '100%', borderWidth: 1, borderColor: C.gray200, marginBottom: 14 },
|
||||
tableHeader: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
backgroundColor: C.white,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: C.gray200,
|
||||
},
|
||||
tableHeaderCell: {
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 6.6,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
},
|
||||
tableHeaderCellCfg: { paddingHorizontal: 6 },
|
||||
tableHeaderCellDivider: { borderRightWidth: 1, borderRightColor: C.gray200 },
|
||||
tableRow: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: C.gray200,
|
||||
},
|
||||
tableRowAlt: { backgroundColor: C.offWhite },
|
||||
tableCell: { paddingVertical: 4, paddingHorizontal: 4, fontSize: 6.6, color: C.gray900 },
|
||||
tableCellCfg: { paddingHorizontal: 6 },
|
||||
tableCellDivider: { borderRightWidth: 1, borderRightColor: C.gray200 },
|
||||
});
|
||||
|
||||
interface ProductData {
|
||||
@@ -276,29 +198,22 @@ interface ProductData {
|
||||
images?: string[];
|
||||
featuredImage?: string | null;
|
||||
categories?: Array<{ name: string }>;
|
||||
attributes?: Array<{
|
||||
name: string;
|
||||
options: string[];
|
||||
}>;
|
||||
attributes?: Array<{ name: string; options: string[] }>;
|
||||
}
|
||||
|
||||
interface PDFDatasheetProps {
|
||||
export interface PDFDatasheetProps {
|
||||
product: ProductData;
|
||||
locale: 'en' | 'de';
|
||||
logoUrl?: string;
|
||||
technicalItems?: any[];
|
||||
voltageTables?: any[];
|
||||
legendItems?: any[];
|
||||
technicalItems?: KeyValueItem[];
|
||||
voltageTables?: DatasheetVoltageTable[];
|
||||
legendItems?: KeyValueItem[];
|
||||
}
|
||||
|
||||
// Helper to strip HTML tags
|
||||
const stripHtml = (html: string): string => {
|
||||
return html.replace(/<[^>]*>/g, '');
|
||||
};
|
||||
const stripHtml = (html: string): string => html.replace(/<[^>]*>/g, '');
|
||||
|
||||
// Helper to get translated labels
|
||||
const getLabels = (locale: 'en' | 'de') => {
|
||||
const labels = {
|
||||
const getLabels = (locale: 'en' | 'de') =>
|
||||
({
|
||||
en: {
|
||||
productDatasheet: 'Technical Datasheet',
|
||||
description: 'APPLICATION',
|
||||
@@ -306,6 +221,9 @@ const getLabels = (locale: 'en' | 'de') => {
|
||||
categories: 'CATEGORIES',
|
||||
sku: 'SKU',
|
||||
noImage: 'No image available',
|
||||
crossSection: 'Configurations',
|
||||
slug_cs: 'Cores & CS',
|
||||
abbreviations: 'ABBREVIATIONS',
|
||||
},
|
||||
de: {
|
||||
productDatasheet: 'Technisches Datenblatt',
|
||||
@@ -314,18 +232,257 @@ const getLabels = (locale: 'en' | 'de') => {
|
||||
categories: 'KATEGORIEN',
|
||||
sku: 'ARTIKELNUMMER',
|
||||
noImage: 'Kein Bild verfügbar',
|
||||
crossSection: 'Konfigurationen',
|
||||
slug_cs: 'Adern & QS',
|
||||
abbreviations: 'ABKÜRZUNGEN',
|
||||
},
|
||||
};
|
||||
return labels[locale];
|
||||
};
|
||||
})[locale];
|
||||
|
||||
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ product, locale }) => {
|
||||
function clamp(n: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
function normTextForMeasure(v: unknown) {
|
||||
return String(v ?? '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
function textLen(v: unknown) {
|
||||
return normTextForMeasure(v).length;
|
||||
}
|
||||
|
||||
function distributeWithMinMax(
|
||||
weights: number[],
|
||||
total: number,
|
||||
minEach: number,
|
||||
maxEach: number,
|
||||
): number[] {
|
||||
const n = weights.length;
|
||||
if (!n) return [];
|
||||
const mins = Array.from({ length: n }, () => minEach);
|
||||
const maxs = Array.from({ length: n }, () => maxEach);
|
||||
const minSum = mins.reduce((a, b) => a + b, 0);
|
||||
if (minSum > total) return mins.map((m) => m * (total / minSum));
|
||||
|
||||
const result = mins.slice();
|
||||
let remaining = total - minSum;
|
||||
let remainingIdx = Array.from({ length: n }, (_, i) => i);
|
||||
|
||||
while (remaining > 1e-9 && remainingIdx.length) {
|
||||
const wSum = remainingIdx.reduce((acc, i) => acc + Math.max(0, weights[i] || 0), 0);
|
||||
if (wSum <= 1e-9) {
|
||||
const even = remaining / remainingIdx.length;
|
||||
for (const i of remainingIdx) result[i] += even;
|
||||
remaining = 0;
|
||||
break;
|
||||
}
|
||||
const nextIdx: number[] = [];
|
||||
for (const i of remainingIdx) {
|
||||
const w = Math.max(0, weights[i] || 0);
|
||||
const add = (w / wSum) * remaining;
|
||||
const capped = Math.min(result[i] + add, maxs[i]);
|
||||
const used = capped - result[i];
|
||||
result[i] = capped;
|
||||
remaining -= used;
|
||||
if (result[i] + 1e-9 < maxs[i]) nextIdx.push(i);
|
||||
}
|
||||
remainingIdx = nextIdx;
|
||||
}
|
||||
const sum = result.reduce((a, b) => a + b, 0);
|
||||
const drift = total - sum;
|
||||
if (Math.abs(drift) > 1e-9) result[result.length - 1] += drift;
|
||||
return result;
|
||||
}
|
||||
|
||||
function KeyValueGrid({ items }: { items: KeyValueItem[] }) {
|
||||
const filtered = (items || []).filter((i) => i.label && i.value);
|
||||
if (!filtered.length) return null;
|
||||
const rows: Array<[KeyValueItem, KeyValueItem | null]> = [];
|
||||
for (let i = 0; i < filtered.length; i += 2) rows.push([filtered[i], filtered[i + 1] || null]);
|
||||
|
||||
return (
|
||||
<View style={styles.kvGrid}>
|
||||
{rows.map(([left, right], rowIndex) => {
|
||||
const isLast = rowIndex === rows.length - 1;
|
||||
const leftValue = left.unit ? `${left.value} ${left.unit}` : left.value;
|
||||
const rightValue = right ? (right.unit ? `${right.value} ${right.unit}` : right.value) : '';
|
||||
return (
|
||||
<View
|
||||
key={`${left.label}-${rowIndex}`}
|
||||
style={[
|
||||
styles.kvRow,
|
||||
rowIndex % 2 === 0 ? styles.kvRowAlt : null,
|
||||
isLast ? styles.kvRowLast : null,
|
||||
]}
|
||||
wrap={false}
|
||||
>
|
||||
<View style={[styles.kvCell, { width: '23%' }]}>
|
||||
<Text style={styles.kvLabelText}>{left.label}</Text>
|
||||
</View>
|
||||
<View style={[styles.kvCell, styles.kvMidDivider, { width: '27%' }]}>
|
||||
<Text style={styles.kvValueText}>{leftValue}</Text>
|
||||
</View>
|
||||
<View style={[styles.kvCell, { width: '23%' }]}>
|
||||
<Text style={styles.kvLabelText}>{right?.label || ''}</Text>
|
||||
</View>
|
||||
<View style={[styles.kvCell, { width: '27%' }]}>
|
||||
<Text style={styles.kvValueText}>{rightValue}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function DenseTable({
|
||||
table,
|
||||
firstColLabel,
|
||||
}: {
|
||||
table: Pick<DatasheetVoltageTable, 'columns' | 'rows'>;
|
||||
firstColLabel: string;
|
||||
}) {
|
||||
const cols = table.columns;
|
||||
const rows = table.rows;
|
||||
const headerText = (label: string) =>
|
||||
String(label || '')
|
||||
.replace(/\s+/g, '\u00A0')
|
||||
.trim();
|
||||
|
||||
const cfgMin = 0.14,
|
||||
cfgMax = 0.23;
|
||||
const cfgContentLen = Math.max(
|
||||
textLen(firstColLabel),
|
||||
...rows.map((r) => textLen(r.configuration)),
|
||||
8,
|
||||
);
|
||||
const dataContentLens = cols.map((c, ci) => {
|
||||
const headerL = textLen(c.label);
|
||||
let cellMax = 0;
|
||||
for (const r of rows) cellMax = Math.max(cellMax, textLen(r.cells[ci]));
|
||||
return Math.max(headerL * 1.15, cellMax, 3);
|
||||
});
|
||||
|
||||
const cfgWeight = cfgContentLen * 1.05;
|
||||
const dataWeights = dataContentLens.map((l) => l);
|
||||
const dataWeightSum = dataWeights.reduce((a, b) => a + b, 0);
|
||||
const rawCfgPct = dataWeightSum > 0 ? cfgWeight / (cfgWeight + dataWeightSum) : 0.28;
|
||||
let cfgPct = clamp(rawCfgPct, cfgMin, cfgMax);
|
||||
|
||||
const minDataPct =
|
||||
cols.length >= 14 ? 0.045 : cols.length >= 12 ? 0.05 : cols.length >= 10 ? 0.055 : 0.06;
|
||||
const cfgPctMaxForMinData = 1 - cols.length * minDataPct;
|
||||
if (Number.isFinite(cfgPctMaxForMinData)) cfgPct = Math.min(cfgPct, cfgPctMaxForMinData);
|
||||
cfgPct = clamp(cfgPct, cfgMin, cfgMax);
|
||||
|
||||
const dataTotal = Math.max(0, 1 - cfgPct);
|
||||
const maxDataPct = Math.min(0.24, Math.max(minDataPct * 2.8, dataTotal * 0.55));
|
||||
const dataPcts = distributeWithMinMax(dataWeights, dataTotal, minDataPct, maxDataPct);
|
||||
|
||||
const cfgW = `${(cfgPct * 100).toFixed(4)}%`;
|
||||
const dataWs = dataPcts.map((p, idx) => {
|
||||
if (idx === dataPcts.length - 1) {
|
||||
const used = dataPcts.slice(0, -1).reduce((a, b) => a + b, 0);
|
||||
const remainder = Math.max(0, dataTotal - used);
|
||||
return `${(remainder * 100).toFixed(4)}%`;
|
||||
}
|
||||
return `${(p * 100).toFixed(4)}%`;
|
||||
});
|
||||
|
||||
const headerFontSize =
|
||||
cols.length >= 14 ? 5.7 : cols.length >= 12 ? 5.9 : cols.length >= 10 ? 6.2 : 6.6;
|
||||
|
||||
return (
|
||||
<View style={styles.tableWrap} break={false}>
|
||||
<View style={styles.tableHeader} wrap={false}>
|
||||
<View style={{ width: cfgW }}>
|
||||
<Text
|
||||
style={[
|
||||
styles.tableHeaderCell,
|
||||
styles.tableHeaderCellCfg,
|
||||
{ fontSize: headerFontSize, paddingHorizontal: 3 },
|
||||
cols.length ? styles.tableHeaderCellDivider : null,
|
||||
]}
|
||||
wrap={false}
|
||||
>
|
||||
{headerText(firstColLabel)}
|
||||
</Text>
|
||||
</View>
|
||||
{cols.map((c, idx) => {
|
||||
const isLast = idx === cols.length - 1;
|
||||
return (
|
||||
<View key={c.key} style={{ width: dataWs[idx] }}>
|
||||
<Text
|
||||
style={[
|
||||
styles.tableHeaderCell,
|
||||
{ fontSize: headerFontSize, paddingHorizontal: 3 },
|
||||
!isLast ? styles.tableHeaderCellDivider : null,
|
||||
]}
|
||||
wrap={false}
|
||||
>
|
||||
{headerText(c.label)}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
{rows.map((r, ri) => (
|
||||
<View
|
||||
key={`${r.configuration}-${ri}`}
|
||||
style={[styles.tableRow, ri % 2 === 0 ? styles.tableRowAlt : null]}
|
||||
wrap={false}
|
||||
minPresenceAhead={16}
|
||||
>
|
||||
<View style={{ width: cfgW }} wrap={false}>
|
||||
<Text
|
||||
style={[
|
||||
styles.tableCell,
|
||||
styles.tableCellCfg,
|
||||
{ fontSize: 6.2, paddingHorizontal: 3 },
|
||||
cols.length ? styles.tableCellDivider : null,
|
||||
]}
|
||||
wrap={false}
|
||||
>
|
||||
{r.configuration}
|
||||
</Text>
|
||||
</View>
|
||||
{r.cells.map((cell, ci) => (
|
||||
<View key={`${cols[ci]?.key || ci}`} style={{ width: dataWs[ci] }} wrap={false}>
|
||||
<Text
|
||||
style={[
|
||||
styles.tableCell,
|
||||
ci !== r.cells.length - 1 ? styles.tableCellDivider : null,
|
||||
]}
|
||||
wrap={false}
|
||||
>
|
||||
{cell}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
|
||||
product,
|
||||
locale,
|
||||
technicalItems = [],
|
||||
voltageTables = [],
|
||||
legendItems = [],
|
||||
}) => {
|
||||
const labels = getLabels(locale);
|
||||
const description = stripHtml(
|
||||
product.applicationHtml ||
|
||||
product.shortDescriptionHtml ||
|
||||
product.descriptionHtml ||
|
||||
product.descriptionText ||
|
||||
'',
|
||||
);
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size="A4" style={styles.page}>
|
||||
{/* Hero Header */}
|
||||
<View style={styles.hero}>
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
@@ -333,18 +490,13 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ product, locale }) =
|
||||
</View>
|
||||
<Text style={styles.docTitle}>{labels.productDatasheet}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.productRow}>
|
||||
<View style={styles.productInfoCol}>
|
||||
<View style={styles.productHero}>
|
||||
<View style={styles.categories}>
|
||||
<Text style={styles.productMeta}>
|
||||
{product.categoriesLine ||
|
||||
(product.categories || []).map((c) => c.name).join(' • ')}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.productName}>{product.name}</Text>
|
||||
</View>
|
||||
<Text style={styles.productMeta}>
|
||||
{product.categoriesLine ||
|
||||
(product.categories || []).map((c) => c.name).join(' • ')}
|
||||
</Text>
|
||||
<Text style={styles.productName}>{product.name}</Text>
|
||||
</View>
|
||||
<View style={styles.productImageCol}>
|
||||
{product.featuredImage ? (
|
||||
@@ -357,64 +509,82 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ product, locale }) =
|
||||
</View>
|
||||
|
||||
<View style={styles.content}>
|
||||
{/* Description section */}
|
||||
{(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && (
|
||||
{description && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{labels.description}</Text>
|
||||
<View style={styles.sectionAccent} />
|
||||
<Text style={styles.description}>
|
||||
{stripHtml(
|
||||
product.applicationHtml ||
|
||||
product.shortDescriptionHtml ||
|
||||
product.descriptionHtml,
|
||||
)}
|
||||
</Text>
|
||||
<Text style={styles.description}>{description}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Technical specifications */}
|
||||
{product.attributes && product.attributes.length > 0 && (
|
||||
{technicalItems.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>
|
||||
<KeyValueGrid items={technicalItems} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Categories as clean tags */}
|
||||
{product.categories && product.categories.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{labels.categories}</Text>
|
||||
{voltageTables.map((table, idx) => (
|
||||
<View key={idx} style={styles.section} break={false}>
|
||||
<Text
|
||||
style={styles.sectionTitle}
|
||||
>{`${labels.crossSection} — ${table.voltageLabel}`}</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>
|
||||
<DenseTable table={table} firstColLabel={labels.slug_cs} />
|
||||
</View>
|
||||
))}
|
||||
|
||||
{legendItems.length > 0 && (
|
||||
<View style={styles.section} break={false}>
|
||||
<Text style={styles.sectionTitle}>{labels.abbreviations}</Text>
|
||||
<View style={styles.sectionAccent} />
|
||||
<KeyValueGrid items={legendItems} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!technicalItems.length &&
|
||||
!voltageTables.length &&
|
||||
product.attributes &&
|
||||
product.attributes.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
|
||||
<View style={styles.sectionAccent} />
|
||||
<View style={{ borderWidth: 1, borderColor: C.gray200 }}>
|
||||
{product.attributes.map((attr, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: index === product.attributes!.length - 1 ? 0 : 1,
|
||||
borderBottomColor: C.gray200,
|
||||
backgroundColor: index % 2 === 0 ? C.offWhite : C.white,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 6,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: C.gray200,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 8.5, fontWeight: 700, color: C.gray600 }}>
|
||||
{attr.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1, padding: 6 }}>
|
||||
<Text style={{ fontSize: 9.5, color: C.gray900 }}>
|
||||
{attr.options.join(', ')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Minimal footer */}
|
||||
<View style={styles.footer} fixed>
|
||||
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
||||
<Text style={styles.footerText}>
|
||||
|
||||
Reference in New Issue
Block a user