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; } // ─── 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 ( {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(/(? (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 ( {parts.map((part, i) => ( {part.text} ))} ); })} ); }; // ─── 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 }) => ( {logo ? : KLZ} {right && {right}} ); const PageFooter: React.FC<{ left: string; right: string; dark?: boolean }> = ({ left, right, dark }) => ( {left} {right} ); // Green accent bar const AccentBar = () => ; // ═══════════════════════════════════════════════════════════════════════════ // PAGE 1: COVER // ═══════════════════════════════════════════════════════════════════════════ const CoverPage: React.FC<{ locale: 'en' | 'de'; introContent?: BrochureProps['introContent']; logoWhite?: string | Buffer; galleryImages?: Array; }> = ({ 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 ( {/* Full-page background image with dark overlay */} {imgValid(bg) && ( )} {!imgValid(bg) && } {/* Vertical accent stripe */} {/* Logo top-left */} {imgValid(logoWhite) ? : KLZ} {/* Main title block — bottom third of page */} {l.catalog} {introContent?.excerpt || l.subtitle} {/* Bottom bar */} {l.edition} {dateStr} www.klz-cables.com ); }; // ═══════════════════════════════════════════════════════════════════════════ // PAGES 2–N: INFO PAGES (each marketing section = own page) // ═══════════════════════════════════════════════════════════════════════════ const InfoPage: React.FC<{ section: NonNullable[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 (
{/* Full-width image at top */} {imgValid(image) && ( {dark && } )} {/* Label + Title */} {section.subtitle} {section.title} {/* Description */} {section.description && ( {section.description} )} {/* Highlights — horizontal stat cards */} {section.highlights && section.highlights.length > 0 && ( {section.highlights.map((h, i) => ( {h.value} {h.label} ))} )} {/* Pull quote */} {section.pullQuote && ( „{section.pullQuote}" )} {/* Items — 2-column grid with accent bars */} {section.items && section.items.length > 0 && ( {section.items.map((item, i) => ( {item.title} {item.description} ))} )} ); }; // 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 (
{/* Full-width image at top */} {imgValid(image) && ( )} {l.about} KLZ Cables {companyInfo.tagline} {/* Values grid */} {l.values} {companyInfo.values.map((v, i) => ( 0{i + 1} {v.title} {v.description} ))} ); }; // ═══════════════════════════════════════════════════════════════════════════ // 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>(); 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 (
{/* Image strip */} {imgValid(image) && ( )} {l.catalog} {l.toc} {Array.from(grouped.entries()).map(([cat, items]) => ( {cat} {items.map((item, i) => ( {item.product.name} {l.page} {item.pageNum} ))} ))} ); }; // ═══════════════════════════════════════════════════════════════════════════ // 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 (
{/* Category + Name */} {product.categories.map(c => c.name).join(' · ')} {product.name} {/* Full-width product image */} {product.featuredImage ? ( ) : ( )} {/* Description + QR in two columns */} {desc && ( {l.application} {desc} )} {(product.qrWebsite || product.qrDatasheet) && ( {product.qrWebsite && ( {l.qrWeb} {locale === 'de' ? 'Produktseite' : 'Product Page'} )} {product.qrDatasheet && ( {l.qrPdf} {locale === 'de' ? 'Datenblatt' : 'Datasheet'} )} )} {/* Technical Data */} {product.attributes && product.attributes.length > 0 && ( {l.specs} {/* Clean table header */} {l.property} {l.value} {product.attributes.map((attr, i) => ( {attr.name} {attr.options.join(', ')} ))} )} ); }; // ═══════════════════════════════════════════════════════════════════════════ // 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 ( {/* Background */} {imgValid(image) && ( )} {!imgValid(image) && } {imgValid(logoWhite) ? ( ) : ( KLZ CABLES )} {l.contact} {companyInfo.address} {companyInfo.phone} {companyInfo.email} {companyInfo.website} © {new Date().getFullYear()} KLZ Cables GmbH ); }; // ═══════════════════════════════════════════════════════════════════════════ // DOCUMENT // ═══════════════════════════════════════════════════════════════════════════ export const PDFBrochure: React.FC = ({ 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 ( {/* About page with image index 1 */} {/* Each marketing section gets its own page */} {marketingSections?.map((section, i) => ( ))} {/* TOC */} {/* Products — each on its own page */} {products.map(p => ( ))} {/* Back cover */} ); };