Files
klz-cables.com/lib/pdf-datasheet.tsx
Marc Mintel 03e597442b
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m36s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Failing after 1m31s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 3s
feat: Centralize OG image font loading and sizing, simplify product page OG generation, and refine template styling.
2026-02-01 11:05:37 +01:00

424 lines
9.8 KiB
TypeScript

import * as React from 'react';
import {
Document,
Page,
View,
Text,
Image,
StyleSheet,
Font,
} from '@react-pdf/renderer';
// 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 },
],
});
// Industrial/technical/restrained design - STYLEGUIDE.md compliant
const styles = StyleSheet.create({
page: {
color: '#111827', // Text Primary
lineHeight: 1.5,
backgroundColor: '#FFFFFF',
paddingTop: 0,
paddingBottom: 100,
fontFamily: 'Helvetica',
},
// Hero-style header
hero: {
backgroundColor: '#FFFFFF',
paddingTop: 24,
paddingBottom: 0,
paddingHorizontal: 72,
marginBottom: 20,
position: 'relative',
borderBottomWidth: 0,
borderBottomColor: '#e5e7eb',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
logoText: {
fontSize: 24,
fontWeight: 700,
color: '#000d26',
letterSpacing: 1,
textTransform: 'uppercase',
},
docTitle: {
fontSize: 10,
fontWeight: 700,
color: '#001a4d',
letterSpacing: 2,
textTransform: 'uppercase',
},
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: '#e5e7eb',
backgroundColor: '#FFFFFF',
overflow: 'hidden',
},
// Product Hero Info
productHero: {
marginTop: 0,
},
productName: {
fontSize: 24,
fontWeight: 700,
color: '#000d26',
marginBottom: 0,
textTransform: 'uppercase',
letterSpacing: -0.5,
},
productMeta: {
fontSize: 10,
color: '#4b5563',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 1,
},
heroImage: {
width: '100%',
height: '100%',
objectFit: 'contain',
},
noImage: {
fontSize: 8,
color: '#9ca3af',
textAlign: 'center',
},
// Content Area
content: {
paddingHorizontal: 72,
},
// Content sections
section: {
marginBottom: 20,
},
sectionTitle: {
fontSize: 14,
fontWeight: 700,
color: '#000d26', // Primary Dark
marginBottom: 8,
textTransform: 'uppercase',
letterSpacing: -0.2,
},
sectionAccent: {
width: 30,
height: 3,
backgroundColor: '#82ed20', // Accent Green
marginBottom: 8,
borderRadius: 1.5,
},
description: {
fontSize: 11,
lineHeight: 1.7,
color: '#4b5563', // Text Secondary
},
// Technical data table
specsTable: {
marginTop: 8,
border: '1px solid #e5e7eb',
borderRadius: 8,
overflow: 'hidden',
},
specsTableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
},
specsTableRowLast: {
borderBottomWidth: 0,
},
specsTableLabelCell: {
flex: 1,
paddingVertical: 4,
paddingHorizontal: 16,
backgroundColor: '#f8f9fa',
borderRightWidth: 1,
borderRightColor: '#e5e7eb',
},
specsTableValueCell: {
flex: 1,
paddingVertical: 4,
paddingHorizontal: 16,
},
specsTableLabelText: {
fontSize: 9,
fontWeight: 700,
color: '#000d26',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
specsTableValueText: {
fontSize: 10,
color: '#111827',
fontWeight: 500,
},
// Categories
categories: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
categoryTag: {
backgroundColor: '#f8f9fa',
paddingHorizontal: 12,
paddingVertical: 6,
border: '1px solid #e5e7eb',
borderRadius: 100,
},
categoryText: {
fontSize: 8,
color: '#4b5563',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
// Footer
footer: {
position: 'absolute',
bottom: 40,
left: 72,
right: 72,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 24,
borderTop: '1px solid #e5e7eb',
},
footerText: {
fontSize: 8,
color: '#9ca3af',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: 1,
},
footerBrand: {
fontSize: 10,
fontWeight: 700,
color: '#000d26',
textTransform: 'uppercase',
letterSpacing: 1,
},
});
interface ProductData {
id: number;
name: string;
shortDescriptionHtml: string;
descriptionHtml: string;
applicationHtml?: string;
images: string[];
featuredImage: string | null;
sku: string;
categories: Array<{ name: string }>;
attributes: Array<{
name: string;
options: string[];
}>;
}
interface PDFDatasheetProps {
product: ProductData;
locale: 'en' | 'de';
logoUrl?: string;
}
// Helper to strip HTML tags
const stripHtml = (html: string): string => {
return html.replace(/<[^>]*>/g, '');
};
// Helper to get translated labels
const getLabels = (locale: 'en' | 'de') => {
const labels = {
en: {
productDatasheet: 'Technical Datasheet',
description: 'APPLICATION',
specifications: 'TECHNICAL DATA',
categories: 'CATEGORIES',
sku: 'SKU',
noImage: 'No image available',
},
de: {
productDatasheet: 'Technisches Datenblatt',
description: 'ANWENDUNG',
specifications: 'TECHNISCHE DATEN',
categories: 'KATEGORIEN',
sku: 'ARTIKELNUMMER',
noImage: 'Kein Bild verfügbar',
},
};
return labels[locale];
};
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
product,
locale,
}) => {
const labels = getLabels(locale);
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Hero Header */}
<View style={styles.hero}>
<View style={styles.header}>
<View>
<Text style={styles.logoText}>KLZ</Text>
</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}>
{product.categories.map((cat, index) => (
<Text key={index} style={styles.productMeta}>
{cat.name}{index < product.categories.length - 1 ? ' • ' : ''}
</Text>
))}
</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 style={styles.content}>
{/* Description section */}
{(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && (
<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>
</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 */}
<View style={styles.footer} fixed>
<Text style={styles.footerBrand}>KLZ CABLES</Text>
<Text style={styles.footerText}>
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</Text>
</View>
</Page>
</Document>
);
};