This commit is contained in:
2026-01-06 22:12:29 +01:00
parent bfdd69533d
commit a73b3db0ed
56 changed files with 2847 additions and 7 deletions

View File

@@ -5,6 +5,21 @@ import react from 'eslint-plugin-react'
import next from '@next/eslint-plugin-next'
export default [
// Global ignores (flat config)
{
ignores: [
'**/node_modules/**',
'.next/**',
'public/datasheets/**',
'data/**',
// Scripts + config files are not part of the frontend lint target.
'scripts/**',
'next.config.*',
'postcss.config.js',
'tailwind.config.*',
'eslint.config.js',
],
},
js.configs.recommended,
{
// Only lint the actual source files, not build output or scripts
@@ -117,4 +132,4 @@ export default [
'no-unused-vars': 'off'
}
}
]
]

437
lib/pdf-datasheet.tsx Normal file
View File

@@ -0,0 +1,437 @@
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: {
padding: 72, // Large margins for engineering documentation feel
fontFamily: 'Helvetica',
fontSize: 10,
color: '#1F2933', // Dark gray text
lineHeight: 1.5, // Generous line height
backgroundColor: '#F8F9FA', // Almost white background
},
// Engineering documentation header
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 48, // Large spacing
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: {
fontSize: 20,
fontWeight: 700,
color: '#0E2A47', // Dark navy
letterSpacing: 1,
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: {
fontSize: 16,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 8,
letterSpacing: 0.5,
textTransform: 'uppercase',
},
skuContainer: {
backgroundColor: '#E6E9ED', // Light gray background
paddingHorizontal: 16,
paddingVertical: 8,
border: '1px solid #E6E9ED',
},
skuLabel: {
fontSize: 8,
color: '#6B7280', // Medium gray
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: {
fontSize: 24,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 12,
lineHeight: 1.2,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
productMeta: {
fontSize: 12,
color: '#6B7280', // Medium gray
fontWeight: 500,
},
// Content sections - rectangular blocks
section: {
marginBottom: 32,
backgroundColor: '#FFFFFF',
padding: 24,
border: '1px solid #E6E9ED',
},
sectionTitle: {
fontSize: 14,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 16,
letterSpacing: 0.5,
textTransform: 'uppercase',
borderBottom: '1px solid #E6E9ED',
paddingBottom: 8,
},
// Description - technical documentation style
description: {
fontSize: 10,
lineHeight: 1.6,
color: '#1F2933', // Dark gray text
marginBottom: 0,
},
// Cross-section table - engineering specification style
table: {
marginTop: 16,
},
tableHeader: {
flexDirection: 'row',
backgroundColor: '#E6E9ED',
borderBottom: '1px solid #E6E9ED',
},
tableHeaderCell: {
flex: 1,
padding: 8,
fontSize: 10,
fontWeight: 700,
color: '#0E2A47',
textTransform: 'uppercase',
letterSpacing: 0.3,
},
tableRow: {
flexDirection: 'row',
borderBottom: '1px solid #F8F9FA',
},
tableCell: {
flex: 1,
padding: 8,
fontSize: 10,
color: '#1F2933',
},
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',
justifyContent: 'space-between',
},
specColumn: {
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: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
categoryTag: {
backgroundColor: '#E6E9ED',
paddingHorizontal: 12,
paddingVertical: 6,
border: '1px solid #E6E9ED',
},
categoryText: {
fontSize: 9,
color: '#6B7280',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: 0.3,
},
// Engineering documentation footer
footer: {
position: 'absolute',
bottom: 48,
left: 72,
right: 72,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 24,
borderTop: '2px solid #E6E9ED',
fontSize: 9,
color: '#6B7280',
},
footerLeft: {
fontWeight: 700,
color: '#0E2A47',
},
footerRight: {
color: '#6B7280',
},
});
interface ProductData {
id: number;
name: string;
shortDescriptionHtml: string;
descriptionHtml: 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: 'Product Datasheet',
description: 'Description',
specifications: 'Technical Specifications',
categories: 'Categories',
sku: 'SKU',
noImage: 'No image available',
},
de: {
productDatasheet: 'Produktdatenblatt',
description: 'Beschreibung',
specifications: 'Technische Spezifikationen',
categories: 'Kategorien',
sku: 'Artikelnummer',
noImage: 'Kein Bild verfügbar',
},
};
return labels[locale];
};
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
product,
locale,
logoUrl = '/media/logo.svg',
}) => {
const labels = getLabels(locale);
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Clean, minimal header */}
<View style={styles.header}>
<View style={styles.logoArea}>
<View style={styles.logoContainer}>
{logoUrl ? (
<Image src={logoUrl} style={styles.logo} />
) : (
<View>
<Text style={styles.logoText}>KLZ</Text>
<Text style={styles.logoSubtext}>Cables</Text>
</View>
)}
</View>
</View>
<View style={styles.docInfo}>
<Text style={styles.docTitle}>
{locale === 'en' ? 'Product Datasheet' : 'Produktdatenblatt'}
</Text>
<View style={styles.skuContainer}>
<Text style={styles.skuLabel}>{labels.sku}</Text>
<Text style={styles.skuValue}>{product.sku}</Text>
</View>
</View>
</View>
{/* Product section - clean and prominent */}
<View style={styles.productSection}>
<Text style={styles.productName}>{product.name}</Text>
<Text style={styles.productMeta}>
{product.categories.map(cat => cat.name).join(' • ')}
</Text>
</View>
{/* 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.specsGrid}>
{product.attributes.map((attr, index) => (
<View key={index} style={styles.specItem}>
<Text style={styles.specLabel}>{attr.name}</Text>
<Text style={styles.specValue}>
{attr.options.join(', ')}
</Text>
</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.categories}>
{product.categories.map((cat, index) => (
<View key={index} style={styles.categoryTag}>
<Text style={styles.categoryText}>{cat.name}</Text>
</View>
))}
</View>
</View>
)}
{/* Minimal footer */}
<View style={styles.footer}>
<Text style={styles.footerLeft}>
{labels.sku}: {product.sku}
</Text>
<Text style={styles.footerRight}>
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</Text>
</View>
</Page>
</Document>
);
};

1126
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@react-pdf/renderer": "^4.3.2",
"@types/cheerio": "^0.22.35",
"axios": "^1.13.2",
"cheerio": "^1.1.2",
@@ -27,9 +28,12 @@
"next": "^14.2.35",
"next-i18next": "^15.4.3",
"next-intl": "^4.6.1",
"pdf-lib": "^1.17.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"resend": "^3.5.0",
"sharp": "^0.34.5",
"svg-to-pdfkit": "^0.1.8",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
@@ -38,6 +42,7 @@
"@types/node": "^22.19.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/sharp": "^0.31.1",
"@vitest/ui": "^4.0.16",
"autoprefixer": "^10.4.23",
"eslint": "^9.17.0",

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long