Files
klz-cables.com/lib/pdf-brochure.tsx
Marc Mintel 17ebde407e
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 2m0s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
feat: product catalog
2026-03-01 22:35:49 +01:00

634 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from 'react';
import {
Document,
Page,
View,
Text,
Image,
} from '@react-pdf/renderer';
// ─── Brand Tokens ───────────────────────────────────────────────────────────
const C = {
navy: '#001a4d',
navyDeep: '#000d26',
green: '#4da612',
greenLight: '#e8f5d8',
white: '#FFFFFF',
offWhite: '#f8f9fa',
gray100: '#f3f4f6',
gray200: '#e5e7eb',
gray300: '#d1d5db',
gray400: '#9ca3af',
gray600: '#4b5563',
gray900: '#111827',
};
const PAGE = { w: 595.28, h: 841.89 }; // A4 in points
const MARGIN = 56;
const CONTENT_W = PAGE.w - MARGIN * 2;
const HEADER_H = 52;
const FOOTER_H = 48;
const BODY_TOP = HEADER_H + 40;
const BODY_BOTTOM = FOOTER_H + 24;
// ─── Types ──────────────────────────────────────────────────────────────────
export interface BrochureProduct {
id: number;
name: string;
shortDescriptionHtml: string;
descriptionHtml: string;
applicationHtml?: string;
images: string[];
featuredImage: string | null;
sku: string;
slug: string;
categories: Array<{ name: string }>;
attributes: Array<{ name: string; options: string[] }>;
qrWebsite?: string | Buffer;
qrDatasheet?: string | Buffer;
}
export interface BrochureProps {
products: BrochureProduct[];
locale: 'en' | 'de';
companyInfo: {
tagline: string;
values: Array<{ title: string; description: string }>;
address: string;
phone: string;
email: string;
website: string;
};
logoBlack?: string | Buffer;
logoWhite?: string | Buffer;
introContent?: { title: string; excerpt: string; heroImage?: string | Buffer };
marketingSections?: Array<{
title: string;
subtitle: string;
description?: string;
items?: Array<{ title: string; description: string }>;
highlights?: Array<{ value: string; label: string }>;
pullQuote?: string;
}>;
galleryImages?: Array<string | Buffer>;
}
// ─── Helpers ────────────────────────────────────────────────────────────────
const strip = (html: string): string => html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
const imgValid = (src?: string | Buffer): boolean => {
if (!src) return false;
if (Buffer.isBuffer(src)) return src.length > 0;
return true;
};
const labels = (locale: 'en' | 'de') => locale === 'de' ? {
catalog: 'Produktkatalog',
subtitle: 'Hochwertige Stromkabel\nMittelspannungslösungen\nSolarkabel',
about: 'Über uns', toc: 'Produktübersicht', overview: 'Produktübersicht',
application: 'Anwendung', specs: 'Technische Daten', contact: 'Kontakt',
qrWeb: 'Web', qrPdf: 'PDF', values: 'Unsere Werte', edition: 'Ausgabe', page: 'S.',
property: 'Eigenschaft', value: 'Wert',
} : {
catalog: 'Product Catalog',
subtitle: 'High-Quality Power Cables\nMedium Voltage Solutions\nSolar Cables',
about: 'About Us', toc: 'Product Overview', overview: 'Product Overview',
application: 'Application', specs: 'Technical Data', contact: 'Contact',
qrWeb: 'Web', qrPdf: 'PDF', values: 'Our Values', edition: 'Edition', page: 'p.',
property: 'Property', value: 'Value',
};
// ─── Rich Text ──────────────────────────────────────────────────────────────
const RichText: React.FC<{ children: string; style?: any; gap?: number; color?: string }> = ({ children, style = {}, gap = 8, color }) => {
const paragraphs = children.split('\n\n').filter(p => p.trim());
return (
<View style={{ gap }}>
{paragraphs.map((para, pIdx) => {
const parts: Array<{ text: string; bold?: boolean; italic?: boolean }> = [];
let rem = para;
while (rem.length > 0) {
const bm = rem.match(/\*\*(.+?)\*\*/);
const im = rem.match(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/);
const first = [bm, im].filter(Boolean).sort((a, b) => (a!.index || 0) - (b!.index || 0))[0];
if (!first || first.index === undefined) { parts.push({ text: rem }); break; }
if (first.index > 0) parts.push({ text: rem.substring(0, first.index) });
parts.push({ text: first[1], bold: first[0].startsWith('**'), italic: !first[0].startsWith('**') });
rem = rem.substring(first.index + first[0].length);
}
return (
<Text key={pIdx} style={style}>
{parts.map((part, i) => (
<Text key={i} style={{
...(part.bold ? { fontWeight: 700, fontFamily: 'Helvetica-Bold', color: color || C.navyDeep } : {}),
...(part.italic ? { fontWeight: 700, fontFamily: 'Helvetica-Bold', color: C.green } : {}),
}}>{part.text}</Text>
))}
</Text>
);
})}
</View>
);
};
// ─── Shared Components ──────────────────────────────────────────────────────
// Thin brand bar at the top of every page
const Header: React.FC<{ logo?: string | Buffer; right?: string; dark?: boolean }> = ({ logo, right, dark }) => (
<View style={{
position: 'absolute', top: 0, left: 0, right: 0, height: HEADER_H,
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end',
paddingHorizontal: MARGIN, paddingBottom: 12,
}} fixed>
{logo ? <Image src={logo} style={{ width: 56 }} /> : <Text style={{ fontSize: 14, fontWeight: 700, color: dark ? C.white : C.navy }}>KLZ</Text>}
{right && <Text style={{ fontSize: 7, fontWeight: 700, color: dark ? C.gray400 : C.gray400, letterSpacing: 1.2, textTransform: 'uppercase' }}>{right}</Text>}
</View>
);
const PageFooter: React.FC<{ left: string; right: string; dark?: boolean }> = ({ left, right, dark }) => (
<View style={{
position: 'absolute', bottom: 20, left: MARGIN, right: MARGIN,
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
borderTopWidth: 0.5, borderTopColor: dark ? 'rgba(255,255,255,0.15)' : C.gray200, borderTopStyle: 'solid',
paddingTop: 8,
}} fixed>
<Text style={{ fontSize: 7, color: dark ? C.gray400 : C.gray400, letterSpacing: 0.8, textTransform: 'uppercase' }}>{left}</Text>
<Text style={{ fontSize: 7, color: dark ? C.gray400 : C.gray400, letterSpacing: 0.8, textTransform: 'uppercase' }}>{right}</Text>
</View>
);
// Green accent bar
const AccentBar = () => <View style={{ width: 40, height: 3, backgroundColor: C.green, marginBottom: 16 }} />;
// ═══════════════════════════════════════════════════════════════════════════
// PAGE 1: COVER
// ═══════════════════════════════════════════════════════════════════════════
const CoverPage: React.FC<{
locale: 'en' | 'de';
introContent?: BrochureProps['introContent'];
logoWhite?: string | Buffer;
galleryImages?: Array<string | Buffer>;
}> = ({ locale, introContent, logoWhite, galleryImages }) => {
const l = labels(locale);
const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', { year: 'numeric', month: 'long' });
const bg = galleryImages?.[0] || introContent?.heroImage;
return (
<Page size="A4" style={{ fontFamily: 'Helvetica' }}>
{/* Full-page background image with dark overlay */}
{imgValid(bg) && (
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
<Image src={bg!} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.navyDeep, opacity: 0.82 }} />
</View>
)}
{!imgValid(bg) && <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.navyDeep }} />}
{/* Vertical accent stripe */}
<View style={{ position: 'absolute', top: 0, left: 0, width: 5, height: '40%', backgroundColor: C.green }} />
{/* Logo top-left */}
<View style={{ position: 'absolute', top: 56, left: MARGIN }}>
{imgValid(logoWhite) ? <Image src={logoWhite!} style={{ width: 120 }} /> : <Text style={{ fontSize: 24, fontWeight: 700, color: C.white }}>KLZ</Text>}
</View>
{/* Main title block — bottom third of page */}
<View style={{ position: 'absolute', bottom: 160, left: MARGIN, right: MARGIN }}>
<View style={{ width: 40, height: 3, backgroundColor: C.green, marginBottom: 24 }} />
<Text style={{ fontSize: 56, fontWeight: 700, color: C.white, textTransform: 'uppercase', letterSpacing: -1, lineHeight: 1 }}>
{l.catalog}
</Text>
<Text style={{ fontSize: 14, color: C.gray300, lineHeight: 1.8, marginTop: 20, maxWidth: 340 }}>
{introContent?.excerpt || l.subtitle}
</Text>
</View>
{/* Bottom bar */}
<View style={{ position: 'absolute', bottom: 40, left: MARGIN, right: MARGIN, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ fontSize: 8, color: C.gray400, letterSpacing: 1, textTransform: 'uppercase' }}>{l.edition} {dateStr}</Text>
<Text style={{ fontSize: 9, fontWeight: 700, color: C.green }}>www.klz-cables.com</Text>
</View>
</Page>
);
};
// ═══════════════════════════════════════════════════════════════════════════
// PAGES 2N: INFO PAGES (each marketing section = own page)
// ═══════════════════════════════════════════════════════════════════════════
const InfoPage: React.FC<{
section: NonNullable<BrochureProps['marketingSections']>[0];
image?: string | Buffer;
logoBlack?: string | Buffer;
dark?: boolean;
}> = ({ section, image, logoBlack, dark }) => {
const bg = dark ? C.navyDeep : C.white;
const textColor = dark ? C.gray300 : C.gray600;
const titleColor = dark ? C.white : C.navyDeep;
const boldColor = dark ? C.white : C.navyDeep;
return (
<Page size="A4" style={{ fontFamily: 'Helvetica', backgroundColor: bg, paddingTop: BODY_TOP, paddingBottom: BODY_BOTTOM, paddingHorizontal: MARGIN }}>
<Header logo={logoBlack} right="KLZ Cables" dark={dark} />
<PageFooter left="KLZ Cables" right="www.klz-cables.com" dark={dark} />
{/* Full-width image at top */}
{imgValid(image) && (
<View style={{ height: 200, marginBottom: 28, marginHorizontal: -MARGIN }}>
<Image src={image!} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
{dark && <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.navyDeep, opacity: 0.15 }} />}
</View>
)}
{/* Label + Title */}
<Text style={{ fontSize: 8, fontWeight: 700, color: C.green, textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 8 }}>{section.subtitle}</Text>
<Text style={{ fontSize: 28, fontWeight: 700, color: titleColor, letterSpacing: -0.5, marginBottom: 8 }}>{section.title}</Text>
<AccentBar />
{/* Description */}
{section.description && (
<View style={{ marginBottom: 24 }}>
<RichText style={{ fontSize: 11, color: textColor, lineHeight: 1.7 }} gap={10} color={boldColor}>
{section.description}
</RichText>
</View>
)}
{/* Highlights — horizontal stat cards */}
{section.highlights && section.highlights.length > 0 && (
<View style={{ flexDirection: 'row', gap: 12, marginBottom: 24 }}>
{section.highlights.map((h, i) => (
<View key={i} style={{
flex: 1,
backgroundColor: dark ? 'rgba(255,255,255,0.04)' : C.offWhite,
borderLeftWidth: 3, borderLeftColor: C.green, borderLeftStyle: 'solid',
paddingVertical: 14, paddingHorizontal: 14,
}}>
<Text style={{ fontSize: 20, fontWeight: 700, color: dark ? C.white : C.navy, marginBottom: 4 }}>{h.value}</Text>
<Text style={{ fontSize: 8, color: dark ? C.gray400 : C.gray600, textTransform: 'uppercase', letterSpacing: 0.5 }}>{h.label}</Text>
</View>
))}
</View>
)}
{/* Pull quote */}
{section.pullQuote && (
<View style={{
borderLeftWidth: 3, borderLeftColor: C.green, borderLeftStyle: 'solid',
paddingLeft: 16, paddingVertical: 8, marginBottom: 24,
}}>
<Text style={{ fontSize: 14, fontWeight: 700, color: titleColor, lineHeight: 1.5 }}>
{section.pullQuote}"
</Text>
</View>
)}
{/* Items — 2-column grid with accent bars */}
{section.items && section.items.length > 0 && (
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 20 }}>
{section.items.map((item, i) => (
<View key={i} style={{ width: '46%' }} minPresenceAhead={60}>
<View style={{ width: 20, height: 2, backgroundColor: C.green, marginBottom: 8 }} />
<Text style={{ fontSize: 10, fontWeight: 700, color: titleColor, marginBottom: 4 }}>{item.title}</Text>
<RichText style={{ fontSize: 9, color: textColor, lineHeight: 1.6 }} gap={4} color={boldColor}>
{item.description}
</RichText>
</View>
))}
</View>
)}
</Page>
);
};
// About page (first info page, special layout with values grid)
const AboutPage: React.FC<{
locale: 'en' | 'de';
companyInfo: BrochureProps['companyInfo'];
logoBlack?: string | Buffer;
image?: string | Buffer;
}> = ({ locale, companyInfo, logoBlack, image }) => {
const l = labels(locale);
return (
<Page size="A4" style={{ fontFamily: 'Helvetica', backgroundColor: C.white, paddingTop: BODY_TOP, paddingBottom: BODY_BOTTOM, paddingHorizontal: MARGIN }}>
<Header logo={logoBlack} right="KLZ Cables" />
<PageFooter left="KLZ Cables" right="www.klz-cables.com" />
{/* Full-width image at top */}
{imgValid(image) && (
<View style={{ height: 220, marginBottom: 28, marginHorizontal: -MARGIN }}>
<Image src={image!} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</View>
)}
<Text style={{ fontSize: 8, fontWeight: 700, color: C.green, textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 8 }}>{l.about}</Text>
<Text style={{ fontSize: 32, fontWeight: 700, color: C.navyDeep, letterSpacing: -0.5, marginBottom: 8 }}>KLZ Cables</Text>
<AccentBar />
<RichText style={{ fontSize: 13, color: C.gray900, lineHeight: 1.8 }} gap={12}>
{companyInfo.tagline}
</RichText>
{/* Values grid */}
<View style={{ marginTop: 32 }}>
<Text style={{ fontSize: 8, fontWeight: 700, color: C.green, textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 16 }}>{l.values}</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 20 }}>
{companyInfo.values.map((v, i) => (
<View key={i} style={{ width: '46%', marginBottom: 8 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 6 }}>
<View style={{ width: 28, height: 28, backgroundColor: C.green, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 12, fontWeight: 700, color: C.white }}>0{i + 1}</Text>
</View>
<Text style={{ fontSize: 11, fontWeight: 700, color: C.navyDeep }}>{v.title}</Text>
</View>
<Text style={{ fontSize: 9, color: C.gray600, lineHeight: 1.6, paddingLeft: 38 }}>{v.description}</Text>
</View>
))}
</View>
</View>
</Page>
);
};
// ═══════════════════════════════════════════════════════════════════════════
// TOC PAGE
// ═══════════════════════════════════════════════════════════════════════════
const TocPage: React.FC<{
products: BrochureProduct[];
locale: 'en' | 'de';
logoBlack?: string | Buffer;
productStartPage: number;
image?: string | Buffer;
}> = ({ products, locale, logoBlack, productStartPage, image }) => {
const l = labels(locale);
const grouped = new Map<string, Array<{ product: BrochureProduct; pageNum: number }>>();
let idx = 0;
for (const p of products) {
const cat = p.categories[0]?.name || 'Other';
if (!grouped.has(cat)) grouped.set(cat, []);
grouped.get(cat)!.push({ product: p, pageNum: productStartPage + idx });
idx++;
}
return (
<Page size="A4" style={{ fontFamily: 'Helvetica', backgroundColor: C.white, paddingTop: BODY_TOP, paddingBottom: BODY_BOTTOM, paddingHorizontal: MARGIN }}>
<Header logo={logoBlack} right={l.overview} />
<PageFooter left="KLZ Cables" right="www.klz-cables.com" />
{/* Image strip */}
{imgValid(image) && (
<View style={{ height: 140, marginBottom: 28, marginHorizontal: -MARGIN }}>
<Image src={image!} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</View>
)}
<Text style={{ fontSize: 8, fontWeight: 700, color: C.green, textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 8 }}>{l.catalog}</Text>
<Text style={{ fontSize: 28, fontWeight: 700, color: C.navyDeep, letterSpacing: -0.5, marginBottom: 8 }}>{l.toc}</Text>
<AccentBar />
{Array.from(grouped.entries()).map(([cat, items]) => (
<View key={cat} style={{ marginBottom: 16 }}>
<View style={{ borderBottomWidth: 1.5, borderBottomColor: C.navy, borderBottomStyle: 'solid', paddingBottom: 4, marginBottom: 6 }}>
<Text style={{ fontSize: 8, fontWeight: 700, color: C.green, textTransform: 'uppercase', letterSpacing: 1.2 }}>{cat}</Text>
</View>
{items.map((item, i) => (
<View key={i} style={{
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
paddingVertical: 5,
borderBottomWidth: i < items.length - 1 ? 0.5 : 0,
borderBottomColor: C.gray200, borderBottomStyle: 'solid',
}}>
<Text style={{ fontSize: 10, fontWeight: 700, color: C.navyDeep }}>{item.product.name}</Text>
<Text style={{ fontSize: 9, color: C.gray400 }}>{l.page} {item.pageNum}</Text>
</View>
))}
</View>
))}
</Page>
);
};
// ═══════════════════════════════════════════════════════════════════════════
// PRODUCT PAGES
// ═══════════════════════════════════════════════════════════════════════════
const ProductPage: React.FC<{
product: BrochureProduct;
locale: 'en' | 'de';
logoBlack?: string | Buffer;
}> = ({ product, locale, logoBlack }) => {
const l = labels(locale);
const desc = strip(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml);
return (
<Page size="A4" style={{ fontFamily: 'Helvetica', backgroundColor: C.white, paddingTop: BODY_TOP, paddingBottom: BODY_BOTTOM, paddingHorizontal: MARGIN }}>
<Header logo={logoBlack} right={l.overview} />
<PageFooter left="KLZ Cables" right="www.klz-cables.com" />
{/* Category + Name */}
<Text style={{ fontSize: 8, fontWeight: 700, color: C.green, textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 8 }}>
{product.categories.map(c => c.name).join(' · ')}
</Text>
<Text style={{ fontSize: 24, fontWeight: 700, color: C.navyDeep, letterSpacing: -0.3, marginBottom: 8 }}>{product.name}</Text>
<AccentBar />
{/* Full-width product image */}
<View style={{
height: 160, marginHorizontal: -MARGIN, marginBottom: 24,
backgroundColor: C.offWhite,
justifyContent: 'center', alignItems: 'center',
padding: 16,
}}>
{product.featuredImage ? (
<Image src={product.featuredImage} style={{ maxWidth: '80%', maxHeight: '100%', objectFit: 'contain' }} />
) : (
<Text style={{ fontSize: 10, color: C.gray400 }}>—</Text>
)}
</View>
{/* Description + QR in two columns */}
<View style={{ flexDirection: 'row', gap: 32, marginBottom: 24 }}>
<View style={{ flex: 2 }}>
{desc && (
<View>
<Text style={{ fontSize: 8, fontWeight: 700, color: C.green, textTransform: 'uppercase', letterSpacing: 1.2, marginBottom: 8 }}>{l.application}</Text>
<RichText style={{ fontSize: 9, color: C.gray600, lineHeight: 1.7 }}>{desc}</RichText>
</View>
)}
</View>
<View style={{ flex: 1 }}>
{(product.qrWebsite || product.qrDatasheet) && (
<View style={{ gap: 14 }}>
{product.qrWebsite && (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<View style={{ borderWidth: 1, borderColor: C.gray200, borderStyle: 'solid', padding: 3 }}>
<Image src={product.qrWebsite} style={{ width: 40, height: 40 }} />
</View>
<View>
<Text style={{ fontSize: 7, fontWeight: 700, color: C.navyDeep, textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 2 }}>{l.qrWeb}</Text>
<Text style={{ fontSize: 7, color: C.gray400 }}>{locale === 'de' ? 'Produktseite' : 'Product Page'}</Text>
</View>
</View>
)}
{product.qrDatasheet && (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<View style={{ borderWidth: 1, borderColor: C.gray200, borderStyle: 'solid', padding: 3 }}>
<Image src={product.qrDatasheet} style={{ width: 40, height: 40 }} />
</View>
<View>
<Text style={{ fontSize: 7, fontWeight: 700, color: C.navyDeep, textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 2 }}>{l.qrPdf}</Text>
<Text style={{ fontSize: 7, color: C.gray400 }}>{locale === 'de' ? 'Datenblatt' : 'Datasheet'}</Text>
</View>
</View>
)}
</View>
)}
</View>
</View>
{/* Technical Data */}
{product.attributes && product.attributes.length > 0 && (
<View>
<Text style={{ fontSize: 8, fontWeight: 700, color: C.green, textTransform: 'uppercase', letterSpacing: 1.2, marginBottom: 10 }}>{l.specs}</Text>
{/* Clean table header */}
<View style={{ flexDirection: 'row', borderBottomWidth: 1.5, borderBottomColor: C.navy, borderBottomStyle: 'solid', paddingBottom: 4, marginBottom: 2 }}>
<View style={{ width: '55%' }}>
<Text style={{ fontSize: 7, fontWeight: 700, color: C.gray400, textTransform: 'uppercase', letterSpacing: 0.8 }}>{l.property}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 7, fontWeight: 700, color: C.gray400, textTransform: 'uppercase', letterSpacing: 0.8 }}>{l.value}</Text>
</View>
</View>
{product.attributes.map((attr, i) => (
<View key={i} style={{
flexDirection: 'row',
paddingVertical: 5,
backgroundColor: i % 2 === 0 ? C.white : C.offWhite,
borderBottomWidth: i < product.attributes.length - 1 ? 0.5 : 0,
borderBottomColor: C.gray200, borderBottomStyle: 'solid',
}}>
<View style={{ width: '55%', paddingRight: 8 }}>
<Text style={{ fontSize: 8, fontWeight: 700, color: C.navyDeep }}>{attr.name}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 9, color: C.gray900 }}>{attr.options.join(', ')}</Text>
</View>
</View>
))}
</View>
)}
</Page>
);
};
// ═══════════════════════════════════════════════════════════════════════════
// BACK COVER
// ═══════════════════════════════════════════════════════════════════════════
const BackCover: React.FC<{
companyInfo: BrochureProps['companyInfo'];
locale: 'en' | 'de';
logoWhite?: string | Buffer;
image?: string | Buffer;
}> = ({ companyInfo, locale, logoWhite, image }) => {
const l = labels(locale);
return (
<Page size="A4" style={{ fontFamily: 'Helvetica' }}>
{/* Background */}
{imgValid(image) && (
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
<Image src={image!} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.navyDeep, opacity: 0.92 }} />
</View>
)}
{!imgValid(image) && <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.navyDeep }} />}
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: MARGIN }}>
{imgValid(logoWhite) ? (
<Image src={logoWhite!} style={{ width: 160, marginBottom: 40 }} />
) : (
<Text style={{ fontSize: 28, fontWeight: 700, color: C.white, letterSpacing: 3, textTransform: 'uppercase', marginBottom: 40 }}>KLZ CABLES</Text>
)}
<View style={{ width: 40, height: 3, backgroundColor: C.green, marginBottom: 40 }} />
<Text style={{ fontSize: 8, fontWeight: 700, color: C.green, textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12 }}>{l.contact}</Text>
<Text style={{ fontSize: 12, color: C.white, lineHeight: 1.8, textAlign: 'center', marginBottom: 20 }}>{companyInfo.address}</Text>
<Text style={{ fontSize: 12, color: C.white, marginBottom: 4 }}>{companyInfo.phone}</Text>
<Text style={{ fontSize: 12, color: C.gray300, marginBottom: 24 }}>{companyInfo.email}</Text>
<Text style={{ fontSize: 13, fontWeight: 700, color: C.green }}>{companyInfo.website}</Text>
</View>
<View style={{ position: 'absolute', bottom: 28, left: MARGIN, right: MARGIN, alignItems: 'center' }} fixed>
<Text style={{ fontSize: 8, color: C.gray400 }}>© {new Date().getFullYear()} KLZ Cables GmbH</Text>
</View>
</Page>
);
};
// ═══════════════════════════════════════════════════════════════════════════
// DOCUMENT
// ═══════════════════════════════════════════════════════════════════════════
export const PDFBrochure: React.FC<BrochureProps> = ({
products, locale, companyInfo, introContent,
marketingSections, logoBlack, logoWhite, galleryImages,
}) => {
// Calculate actual page numbers
// Cover(1) + About(1) + marketingSections.length + TOC(1) + products + BackCover(1)
const numInfoPages = 1 + (marketingSections?.length || 0); // About + sections
const productStartPage = 1 + numInfoPages + 1; // Cover + info pages + TOC
// Assign images to sections: dark sections get indices 2,4; light get 3
const sectionThemes: Array<'light' | 'dark'> = [];
if (marketingSections) {
for (let i = 0; i < marketingSections.length; i++) {
// Alternate: light, dark, light, dark, light, dark
sectionThemes.push(i % 2 === 1 ? 'dark' : 'light');
}
}
return (
<Document>
<CoverPage locale={locale} introContent={introContent} logoWhite={logoWhite} galleryImages={galleryImages} />
{/* About page with image index 1 */}
<AboutPage locale={locale} companyInfo={companyInfo} logoBlack={logoBlack} image={galleryImages?.[1]} />
{/* Each marketing section gets its own page */}
{marketingSections?.map((section, i) => (
<InfoPage
key={`info-${i}`}
section={section}
image={galleryImages?.[i + 2]}
logoBlack={logoBlack}
dark={sectionThemes[i] === 'dark'}
/>
))}
{/* TOC */}
<TocPage products={products} locale={locale} logoBlack={logoBlack} productStartPage={productStartPage} image={galleryImages?.[5]} />
{/* Products — each on its own page */}
{products.map(p => (
<ProductPage key={p.id} product={p} locale={locale} logoBlack={logoBlack} />
))}
{/* Back cover */}
<BackCover companyInfo={companyInfo} locale={locale} logoWhite={logoWhite} image={galleryImages?.[6]} />
</Document>
);
};