From 437dd35c9cbfd546112013814bcef964c9942202 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 1 Mar 2026 13:07:13 +0100 Subject: [PATCH] feat: product catalog --- lib/pdf-brochure.tsx | 529 +++++++++++---------- public/brochure/klz-product-catalog-de.pdf | Bin 9808937 -> 10057172 bytes public/brochure/klz-product-catalog-en.pdf | Bin 9809581 -> 10057396 bytes 3 files changed, 286 insertions(+), 243 deletions(-) diff --git a/lib/pdf-brochure.tsx b/lib/pdf-brochure.tsx index 03620fe3..c88d3708 100644 --- a/lib/pdf-brochure.tsx +++ b/lib/pdf-brochure.tsx @@ -11,10 +11,10 @@ import { // ─── Brand Colors ─────────────────────────────────────────────────────────── const C = { - primary: '#001a4d', - primaryDark: '#000d26', + primary: '#001a4d', // Navy + primaryDark: '#000d26', // Deepest Navy saturated: '#0117bf', - accent: '#4da612', + accent: '#4da612', // Green accentLight: '#e8f5d8', black: '#000000', white: '#FFFFFF', @@ -94,10 +94,21 @@ const L = (locale: 'en' | 'de') => locale === 'de' ? { 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 }> = ({ children, style = {}, paragraphGap = 8 }) => { - const paragraphs = children.split('\n\n').filter(p => p.trim()); +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) => { @@ -116,7 +127,7 @@ const RichText: React.FC<{ children: string; style?: any; paragraphGap?: number {parts.map((part, i) => ( {part.text} ))} @@ -127,105 +138,159 @@ const RichText: React.FC<{ children: string; style?: any; paragraphGap?: number ); }; -// ─── Text tokens ──────────────────────────────────────────────────────────── - -const T = { - label: { fontSize: 9 as number, fontWeight: 700 as const, color: C.accent, textTransform: 'uppercase' as const, letterSpacing: 1.2 }, - sectionTitle: { fontSize: 16 as number, fontWeight: 700 as const, color: C.primaryDark, letterSpacing: -0.3 }, - body: { fontSize: 10 as number, color: C.gray600, lineHeight: 1.7 }, - bodyLead: { fontSize: 11 as number, color: C.gray900, lineHeight: 1.8 }, - bodyBold: { fontSize: 10 as number, fontWeight: 700 as const, color: C.primaryDark }, - caption: { fontSize: 8 as number, color: C.gray400, textTransform: 'uppercase' as const, letterSpacing: 1 }, -}; - // ─── Reusable Components ──────────────────────────────────────────────────── -const FixedHeader: React.FC<{ logoBlack?: string | Buffer; rightText?: string }> = ({ logoBlack, rightText }) => ( - - {logoBlack ? : KLZ} - {rightText && {rightText}} - -); - -const Footer: React.FC<{ left: string; right: string; logoBlack?: string | Buffer }> = ({ left, right, logoBlack }) => ( - - - {logoBlack && } - {left} +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}} - {right} - -); + ); +}; -const SectionHeading: React.FC<{ label?: string; title: string }> = ({ label, title }) => ( - - {label && {label}} - {title} - +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 }> = ({ quote }) => ( +const PullQuote: React.FC<{ quote: string, isDark: boolean }> = ({ quote, isDark }) => ( - + „{quote}" ); // Stat highlight boxes -const HighlightRow: React.FC<{ highlights: Array<{ value: string; label: string }> }> = ({ highlights }) => ( - +const HighlightRow: React.FC<{ highlights: Array<{ value: string; label: string }>, isDark: boolean }> = ({ highlights, isDark }) => ( + {highlights.map((h, i) => ( - {h.value} - {h.label} + {h.value} + {h.label} ))} ); -// Inline image strip between sections -const SectionImage: React.FC<{ src: string | Buffer; height?: number }> = ({ src, height = 100 }) => { - // Skip empty buffers +// 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 && ( + + )} ); }; -// Horizontal divider -const Divider: React.FC = () => ( - -); +// 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 ───────────────────────────────────────────────────────────── @@ -233,145 +298,121 @@ const CoverPage: React.FC<{ locale: 'en' | 'de'; introContent?: BrochureProps['introContent']; logoBlack?: string | Buffer; + logoWhite?: string | Buffer; galleryImages?: Array; -}> = ({ locale, introContent, logoBlack, galleryImages }) => { +}> = ({ 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 && ( - + )} - + - {logoBlack ? : KLZ} + {logo ? : KLZ} - - - + + + {l.catalog} - + {introContent?.excerpt || l.subtitle} - {l.edition} {dateStr} - www.klz-cables.com + {l.edition} {dateStr} + www.klz-cables.com ); }; -// ─── Info Flow (NO repeated background image — uses inline images) ────────── +// ─── Info Flow ────────────────────────────────────────────────────────────── const InfoFlow: React.FC<{ locale: 'en' | 'de'; companyInfo: BrochureProps['companyInfo']; - introContent?: BrochureProps['introContent']; marketingSections?: BrochureProps['marketingSections']; logoBlack?: string | Buffer; galleryImages?: Array; -}> = ({ locale, companyInfo, introContent, marketingSections, logoBlack, galleryImages }) => { +}> = ({ locale, companyInfo, marketingSections, logoBlack, galleryImages }) => { const l = L(locale); return ( - -