import * as React from 'react'; import { Document, Page, View, Text, Image, StyleSheet, } from '@react-pdf/renderer'; // ─── Brand Colors ─────────────────────────────────────────────────────────── const C = { primary: '#001a4d', // Navy primaryDark: '#000d26', // Deepest Navy saturated: '#0117bf', accent: '#4da612', // Green accentLight: '#e8f5d8', black: '#000000', white: '#FFFFFF', gray050: '#f8f9fa', gray100: '#f3f4f6', gray200: '#e5e7eb', gray300: '#d1d5db', gray400: '#9ca3af', gray600: '#4b5563', gray900: '#111827', }; // ─── Spacing Scale ────────────────────────────────────────────────────────── const S = { xs: 4, sm: 8, md: 16, lg: 24, xl: 40, xxl: 56 } as const; const M = { h: 72, bottom: 96 } as const; const HEADER_H = 64; const PAGE_TOP_PADDING = 110; // ─── 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; } // ─── Helpers ──────────────────────────────────────────────────────────────── const stripHtml = (html: string): string => html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); const L = (locale: 'en' | 'de') => locale === 'de' ? { catalog: 'Produktkatalog', subtitle: 'Hochwertige Stromkabel · Mittelspannungslösungen · Solarkabel', 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.', } : { catalog: 'Product Catalog', subtitle: 'High-Quality Power Cables · Medium Voltage Solutions · Solar 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.', }; // ─── Text tokens ──────────────────────────────────────────────────────────── const T = { label: (d: boolean) => ({ fontSize: 9, fontWeight: 700, color: C.accent, textTransform: 'uppercase' as 'uppercase', letterSpacing: 1.2 }), sectionTitle: (d: boolean) => ({ fontSize: 24, fontWeight: 700, color: d ? C.white : C.primaryDark, letterSpacing: -0.5 }), body: (d: boolean) => ({ fontSize: 10, color: d ? C.gray300 : C.gray600, lineHeight: 1.7 }), bodyLead: (d: boolean) => ({ fontSize: 13, color: d ? C.white : C.gray900, lineHeight: 1.8 }), bodyBold: (d: boolean) => ({ fontSize: 10, fontWeight: 700, color: d ? C.white : C.primaryDark }), caption: (d: boolean) => ({ fontSize: 8, color: d ? C.gray400 : C.gray400, textTransform: 'uppercase' as 'uppercase', letterSpacing: 1 }), }; // ─── Rich Text (supports **bold** and *italic*) ──────────────────────────── const RichText: React.FC<{ children: string; style?: any; paragraphGap?: number; isDark?: boolean; asParagraphs?: boolean }> = ({ children, style = {}, paragraphGap = 8, isDark = false, asParagraphs = true }) => { const paragraphs = asParagraphs ? children.split('\n\n').filter(p => p.trim()) : [children]; return ( {paragraphs.map((para, pIdx) => { const parts: Array<{ text: string; bold?: boolean; italic?: boolean }> = []; let remaining = para; while (remaining.length > 0) { const boldMatch = remaining.match(/\*\*(.+?)\*\*/); const italicMatch = remaining.match(/(? (a!.index || 0) - (b!.index || 0))[0]; if (!firstMatch || firstMatch.index === undefined) { parts.push({ text: remaining }); break; } if (firstMatch.index > 0) parts.push({ text: remaining.substring(0, firstMatch.index) }); parts.push({ text: firstMatch[1], bold: firstMatch[0].startsWith('**'), italic: !firstMatch[0].startsWith('**') }); remaining = remaining.substring(firstMatch.index + firstMatch[0].length); } return ( {parts.map((part, i) => ( {part.text} ))} ); })} ); }; // ─── Reusable Components ──────────────────────────────────────────────────── const FixedHeader: React.FC<{ logoWhite?: string | Buffer; logoBlack?: string | Buffer; rightText?: string; isDark?: boolean }> = ({ logoWhite, logoBlack, rightText, isDark }) => { const logo = isDark ? (logoWhite || logoBlack) : logoBlack; return ( {logo ? : KLZ} {rightText && {rightText}} ); }; const Footer: React.FC<{ left: string; right: string; logoWhite?: string | Buffer; logoBlack?: string | Buffer; isDark?: boolean }> = ({ left, right, logoWhite, logoBlack, isDark }) => { const logo = isDark ? (logoWhite || logoBlack) : logoBlack; return ( {logo && } {left} {right} ); }; const SectionHeading: React.FC<{ label?: string; title: string, isDark: boolean }> = ({ label, title, isDark }) => ( {label && {label}} {title} ); // Pull-quote callout block const PullQuote: React.FC<{ quote: string, isDark: boolean }> = ({ quote, isDark }) => ( „{quote}" ); // Stat highlight boxes const HighlightRow: React.FC<{ highlights: Array<{ value: string; label: string }>, isDark: boolean }> = ({ highlights, isDark }) => ( {highlights.map((h, i) => ( {h.value} {h.label} ))} ); // Magazine Edge-to-Edge Image // By using negative horizontal margin (-M.h) matching the parent padding, it touches the very edges. // By using negative vertical margin matching the vertical padding, it touches the top or bottom of the colored block! const MagazineImage: React.FC<{ src: string | Buffer; height?: number; position: 'top' | 'bottom' | 'middle'; isDark?: boolean }> = ({ src, height = 260, position, isDark }) => { if (Buffer.isBuffer(src) && src.length === 0) return null; const marginTop = position === 'top' ? -S.xxl : (position === 'middle' ? S.xl : S.xxl); const marginBottom = position === 'bottom' ? -S.xxl : (position === 'middle' ? S.xl : S.xxl); return ( {isDark && ( )} ); }; // Magazine Block wrapper const MagazineSection: React.FC<{ section: NonNullable[0]; image?: string | Buffer; theme: 'white' | 'gray' | 'dark'; imagePosition: 'top' | 'bottom' | 'middle'; }> = ({ section, image, theme, imagePosition }) => { const isDark = theme === 'dark'; const bgColor = theme === 'white' ? C.white : (theme === 'gray' ? C.gray050 : C.primaryDark); return ( {image && imagePosition === 'top' && } {section.description && ( {section.description} )} {image && imagePosition === 'middle' && } {section.highlights && section.highlights.length > 0 && ( )} {section.pullQuote && } {section.items && section.items.length > 0 && ( {section.items.map((item, i) => ( {item.title} {item.description} ))} )} {image && imagePosition === 'bottom' && } ); }; // ─── Cover Page ───────────────────────────────────────────────────────────── const CoverPage: React.FC<{ locale: 'en' | 'de'; introContent?: BrochureProps['introContent']; logoBlack?: string | Buffer; logoWhite?: string | Buffer; galleryImages?: Array; }> = ({ locale, introContent, logoWhite, logoBlack, galleryImages }) => { const l = L(locale); const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', { year: 'numeric', month: 'long' }); const bgImage = galleryImages?.[0] || introContent?.heroImage; const logo = logoWhite || logoBlack; return ( {bgImage && ( )} {logo ? : KLZ} {l.catalog} {introContent?.excerpt || l.subtitle} {l.edition} {dateStr} www.klz-cables.com ); }; // ─── Info Flow ────────────────────────────────────────────────────────────── const InfoFlow: React.FC<{ locale: 'en' | 'de'; companyInfo: BrochureProps['companyInfo']; marketingSections?: BrochureProps['marketingSections']; logoBlack?: string | Buffer; galleryImages?: Array; }> = ({ locale, companyInfo, marketingSections, logoBlack, galleryImages }) => { const l = L(locale); return (