Merge branch 'main' into feature/strapi

This commit is contained in:
2026-01-30 22:10:16 +01:00
130 changed files with 728 additions and 587 deletions

View File

@@ -21,234 +21,149 @@ Font.register({
// Industrial/technical/restrained design - STYLEGUIDE.md compliant
const styles = StyleSheet.create({
page: {
// Large margins for engineering documentation feel.
// Extra bottom padding reserves space for the fixed footer so content
// (esp. long descriptions) doesn't render underneath it.
paddingTop: 72,
paddingLeft: 72,
paddingRight: 72,
paddingBottom: 140,
color: '#111827', // Text Primary
lineHeight: 1.5,
backgroundColor: '#FFFFFF',
paddingTop: 0,
paddingBottom: 100,
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: {
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',
alignItems: 'center',
marginBottom: 16,
},
logoText: {
fontSize: 20,
fontSize: 24,
fontWeight: 700,
color: '#0E2A47', // Dark navy
color: '#000d26',
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,
fontSize: 10,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 8,
letterSpacing: 0.5,
color: '#001a4d',
letterSpacing: 2,
textTransform: 'uppercase',
},
skuContainer: {
backgroundColor: '#E6E9ED', // Light gray background
paddingHorizontal: 16,
paddingVertical: 8,
border: '1px solid #E6E9ED',
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',
},
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',
// Product Hero Info
productHero: {
marginTop: 0,
},
productName: {
fontSize: 24,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 12,
lineHeight: 1.2,
color: '#000d26',
marginBottom: 0,
textTransform: 'uppercase',
letterSpacing: 0.5,
letterSpacing: -0.5,
},
productMeta: {
fontSize: 12,
color: '#6B7280', // Medium gray
fontWeight: 500,
fontSize: 10,
color: '#4b5563',
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: {
marginBottom: 32,
backgroundColor: '#FFFFFF',
padding: 24,
border: '1px solid #E6E9ED',
marginBottom: 20,
},
sectionTitle: {
fontSize: 14,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 16,
letterSpacing: 0.5,
color: '#000d26', // Primary Dark
marginBottom: 8,
textTransform: 'uppercase',
borderBottom: '1px solid #E6E9ED',
paddingBottom: 8,
letterSpacing: -0.2,
},
sectionAccent: {
width: 30,
height: 3,
backgroundColor: '#82ed20', // Accent Green
marginBottom: 8,
borderRadius: 1.5,
},
// Description - technical documentation style
description: {
fontSize: 10,
lineHeight: 1.6,
color: '#1F2933', // Dark gray text
marginBottom: 0,
fontSize: 11,
lineHeight: 1.7,
color: '#4b5563', // Text Secondary
},
// Cross-section table - engineering specification style
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)
// Technical data table
specsTable: {
borderWidth: 1,
borderColor: '#E6E9ED',
marginTop: 8,
border: '1px solid #e5e7eb',
borderRadius: 8,
overflow: 'hidden',
},
specsTableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
borderBottomColor: '#e5e7eb',
},
specsTableRowLast: {
@@ -256,63 +171,35 @@ const styles = StyleSheet.create({
},
specsTableLabelCell: {
flex: 3,
paddingVertical: 8,
paddingHorizontal: 8,
backgroundColor: '#F8F9FA',
flex: 1,
paddingVertical: 4,
paddingHorizontal: 16,
backgroundColor: '#f8f9fa',
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
justifyContent: 'center',
borderRightColor: '#e5e7eb',
},
specsTableValueCell: {
flex: 4,
paddingVertical: 8,
paddingHorizontal: 8,
justifyContent: 'center',
flex: 1,
paddingVertical: 4,
paddingHorizontal: 16,
},
specsTableLabelText: {
fontSize: 9,
fontWeight: 700,
color: '#0E2A47',
color: '#000d26',
textTransform: 'uppercase',
letterSpacing: 0.3,
lineHeight: 1.2,
letterSpacing: 0.5,
},
specsTableValueText: {
fontSize: 10,
color: '#1F2933',
lineHeight: 1.4,
color: '#111827',
fontWeight: 500,
},
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
categories: {
flexDirection: 'row',
flexWrap: 'wrap',
@@ -320,42 +207,48 @@ const styles = StyleSheet.create({
},
categoryTag: {
backgroundColor: '#E6E9ED',
backgroundColor: '#f8f9fa',
paddingHorizontal: 12,
paddingVertical: 6,
border: '1px solid #E6E9ED',
border: '1px solid #e5e7eb',
borderRadius: 100,
},
categoryText: {
fontSize: 9,
color: '#6B7280',
fontWeight: 500,
fontSize: 8,
color: '#4b5563',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 0.3,
letterSpacing: 0.5,
},
// Engineering documentation footer
// Footer
footer: {
position: 'absolute',
bottom: 48,
bottom: 40,
left: 72,
right: 72,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 24,
borderTop: '2px solid #E6E9ED',
fontSize: 9,
color: '#6B7280',
borderTop: '1px solid #e5e7eb',
},
footerLeft: {
footerText: {
fontSize: 8,
color: '#9ca3af',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: 1,
},
footerBrand: {
fontSize: 10,
fontWeight: 700,
color: '#0E2A47',
},
footerRight: {
color: '#6B7280',
color: '#000d26',
textTransform: 'uppercase',
letterSpacing: 1,
},
});
@@ -364,6 +257,7 @@ interface ProductData {
name: string;
shortDescriptionHtml: string;
descriptionHtml: string;
applicationHtml?: string;
images: string[];
featuredImage: string | null;
sku: string;
@@ -389,19 +283,19 @@ const stripHtml = (html: string): string => {
const getLabels = (locale: 'en' | 'de') => {
const labels = {
en: {
productDatasheet: 'Product Datasheet',
description: 'Description',
specifications: 'Technical Specifications',
categories: 'Categories',
productDatasheet: 'Technical Datasheet',
description: 'APPLICATION',
specifications: 'TECHNICAL DATA',
categories: 'CATEGORIES',
sku: 'SKU',
noImage: 'No image available',
},
de: {
productDatasheet: 'Produktdatenblatt',
description: 'Beschreibung',
specifications: 'Technische Spezifikationen',
categories: 'Kategorien',
sku: 'Artikelnummer',
productDatasheet: 'Technisches Datenblatt',
description: 'ANWENDUNG',
specifications: 'TECHNISCHE DATEN',
categories: 'KATEGORIEN',
sku: 'ARTIKELNUMMER',
noImage: 'Kein Bild verfügbar',
},
};
@@ -418,99 +312,101 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Clean, minimal header */}
<View style={styles.header}>
<View style={styles.logoArea}>
<View style={styles.logoContainer}>
{logoUrl ? (
/* 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>
{/* 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 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 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>
{/* 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>
</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.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} fixed>
<Text style={styles.footerLeft}>
{labels.sku}: {product.sku}
</Text>
<Text style={styles.footerRight}>
<Text style={styles.footerBrand}>KLZ CABLES</Text>
<Text style={styles.footerText}>
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
year: 'numeric',
month: 'long',