Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 2m15s
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
650 lines
32 KiB
TypeScript
650 lines
32 KiB
TypeScript
import * as React from 'react';
|
|
import {
|
|
Document,
|
|
Page,
|
|
View,
|
|
Text,
|
|
Image,
|
|
StyleSheet,
|
|
} from '@react-pdf/renderer';
|
|
|
|
// ─── Brand Colors ───────────────────────────────────────────────────────────
|
|
|
|
const C = {
|
|
primary: '#001a4d',
|
|
primaryDark: '#000d26',
|
|
saturated: '#0117bf',
|
|
accent: '#4da612',
|
|
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<string | Buffer>;
|
|
}
|
|
|
|
// ─── 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.',
|
|
};
|
|
|
|
// ─── 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());
|
|
return (
|
|
<View style={{ gap: paragraphGap }}>
|
|
{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(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/);
|
|
const firstMatch = [boldMatch, italicMatch].filter(Boolean).sort((a, b) => (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 (
|
|
<Text key={pIdx} style={style}>
|
|
{parts.map((part, i) => (
|
|
<Text key={i} style={{
|
|
...(part.bold ? { fontWeight: 700, fontFamily: 'Helvetica-Bold', color: C.primaryDark } : {}),
|
|
...(part.italic ? { fontWeight: 700, fontFamily: 'Helvetica-Bold', color: C.accent } : {}),
|
|
}}>{part.text}</Text>
|
|
))}
|
|
</Text>
|
|
);
|
|
})}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// ─── 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 }) => (
|
|
<View style={{
|
|
position: 'absolute', top: 0, left: 0, right: 0, height: HEADER_H,
|
|
paddingHorizontal: M.h, paddingTop: 24, paddingBottom: 16,
|
|
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
|
|
borderBottomWidth: 0.5, borderBottomColor: C.gray300, borderBottomStyle: 'solid',
|
|
backgroundColor: C.white,
|
|
}} fixed>
|
|
{logoBlack ? <Image src={logoBlack} style={{ width: 64 }} /> : <Text style={{ fontSize: 16, fontWeight: 700, color: C.primaryDark }}>KLZ</Text>}
|
|
{rightText && <Text style={{ fontSize: 8, fontWeight: 700, color: C.primary, letterSpacing: 0.8, textTransform: 'uppercase' }}>{rightText}</Text>}
|
|
</View>
|
|
);
|
|
|
|
const Footer: React.FC<{ left: string; right: string; logoBlack?: string | Buffer }> = ({ left, right, logoBlack }) => (
|
|
<View style={{
|
|
position: 'absolute', bottom: 40, left: M.h, right: M.h,
|
|
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
|
|
paddingTop: 12,
|
|
borderTopWidth: 0.5, borderTopColor: C.gray300, borderTopStyle: 'solid',
|
|
}} fixed>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
{logoBlack && <Image src={logoBlack} style={{ width: 40, height: 'auto' }} />}
|
|
<Text style={T.caption}>{left}</Text>
|
|
</View>
|
|
<Text style={T.caption}>{right}</Text>
|
|
</View>
|
|
);
|
|
|
|
const SectionHeading: React.FC<{ label?: string; title: string }> = ({ label, title }) => (
|
|
<View style={{ marginBottom: S.md }} minPresenceAhead={120}>
|
|
{label && <Text style={{ ...T.label, marginBottom: S.sm }}>{label}</Text>}
|
|
<Text style={{ ...T.sectionTitle, marginBottom: S.sm }}>{title}</Text>
|
|
<View style={{ width: 32, height: 3, backgroundColor: C.accent, borderRadius: 1.5 }} />
|
|
</View>
|
|
);
|
|
|
|
// Pull-quote callout block
|
|
const PullQuote: React.FC<{ quote: string }> = ({ quote }) => (
|
|
<View style={{
|
|
borderLeftWidth: 3, borderLeftColor: C.accent, borderLeftStyle: 'solid',
|
|
paddingLeft: S.md, paddingVertical: S.sm,
|
|
marginVertical: S.md,
|
|
}}>
|
|
<Text style={{ fontSize: 12, fontWeight: 700, color: C.primaryDark, lineHeight: 1.6 }}>
|
|
„{quote}"
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
// Stat highlight boxes
|
|
const HighlightRow: React.FC<{ highlights: Array<{ value: string; label: string }> }> = ({ highlights }) => (
|
|
<View style={{ flexDirection: 'row', gap: S.md, marginVertical: S.md }}>
|
|
{highlights.map((h, i) => (
|
|
<View key={i} style={{
|
|
flex: 1,
|
|
backgroundColor: C.gray050,
|
|
borderWidth: 0.5, borderColor: C.gray200, borderStyle: 'solid',
|
|
borderRadius: 4,
|
|
paddingVertical: S.sm, paddingHorizontal: S.md,
|
|
alignItems: 'center',
|
|
}}>
|
|
<Text style={{ fontSize: 12, fontWeight: 700, color: C.accent, marginBottom: 2 }}>{h.value}</Text>
|
|
<Text style={{ fontSize: 8, color: C.gray600, textAlign: 'center' }}>{h.label}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
|
|
// Inline image strip between sections
|
|
const SectionImage: React.FC<{ src: string | Buffer; height?: number }> = ({ src, height = 100 }) => {
|
|
// Skip empty buffers
|
|
if (Buffer.isBuffer(src) && src.length === 0) return null;
|
|
return (
|
|
<View style={{
|
|
width: '100%', height, borderRadius: 4, overflow: 'hidden',
|
|
marginVertical: S.md,
|
|
}}>
|
|
<Image src={src} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// Horizontal divider
|
|
const Divider: React.FC = () => (
|
|
<View style={{ borderTopWidth: 0.5, borderTopColor: C.gray200, borderTopStyle: 'solid', marginVertical: S.lg }} />
|
|
);
|
|
|
|
// ─── Cover Page ─────────────────────────────────────────────────────────────
|
|
|
|
const CoverPage: React.FC<{
|
|
locale: 'en' | 'de';
|
|
introContent?: BrochureProps['introContent'];
|
|
logoBlack?: string | Buffer;
|
|
galleryImages?: Array<string | Buffer>;
|
|
}> = ({ locale, introContent, 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;
|
|
|
|
return (
|
|
<Page size="A4" style={{ backgroundColor: C.white, fontFamily: 'Helvetica' }}>
|
|
{bgImage && (
|
|
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
|
|
<Image src={bgImage} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.white, opacity: 0.82 }} />
|
|
</View>
|
|
)}
|
|
<View style={{ position: 'absolute', top: 0, left: 0, width: 4, height: 280, backgroundColor: C.accent }} />
|
|
|
|
<View style={{ flex: 1, paddingHorizontal: M.h }}>
|
|
<View style={{ marginTop: 80 }}>
|
|
{logoBlack ? <Image src={logoBlack} style={{ width: 140 }} /> : <Text style={{ fontSize: 28, fontWeight: 700, color: C.black }}>KLZ</Text>}
|
|
</View>
|
|
|
|
<View style={{ marginTop: 160 }}>
|
|
<View style={{ width: 48, height: 4, backgroundColor: C.accent, borderRadius: 2, marginBottom: S.lg }} />
|
|
<Text style={{ fontSize: 42, fontWeight: 700, color: C.black, textTransform: 'uppercase', letterSpacing: 0.5, lineHeight: 1.1, marginBottom: S.md }}>
|
|
{l.catalog}
|
|
</Text>
|
|
<Text style={{ fontSize: 14, color: C.gray600, lineHeight: 1.6, maxWidth: 360 }}>
|
|
{introContent?.excerpt || l.subtitle}
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={{ position: 'absolute', bottom: S.xxl, left: M.h, right: M.h, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<Text style={T.caption}>{l.edition} {dateStr}</Text>
|
|
<Text style={T.caption}>www.klz-cables.com</Text>
|
|
</View>
|
|
</View>
|
|
</Page>
|
|
);
|
|
};
|
|
|
|
// ─── Info Flow (NO repeated background image — uses inline images) ──────────
|
|
|
|
const InfoFlow: React.FC<{
|
|
locale: 'en' | 'de';
|
|
companyInfo: BrochureProps['companyInfo'];
|
|
introContent?: BrochureProps['introContent'];
|
|
marketingSections?: BrochureProps['marketingSections'];
|
|
logoBlack?: string | Buffer;
|
|
galleryImages?: Array<string | Buffer>;
|
|
}> = ({ locale, companyInfo, introContent, marketingSections, logoBlack, galleryImages }) => {
|
|
const l = L(locale);
|
|
|
|
return (
|
|
<Page size="A4" wrap style={{ backgroundColor: C.white, fontFamily: 'Helvetica', paddingTop: PAGE_TOP_PADDING, paddingBottom: M.bottom }}>
|
|
<FixedHeader logoBlack={logoBlack} rightText="KLZ Cables" />
|
|
<Footer left="KLZ Cables" right={companyInfo.website} logoBlack={logoBlack} />
|
|
|
|
<View style={{ paddingHorizontal: M.h }}>
|
|
|
|
{/* ── About KLZ ── */}
|
|
<View style={{ marginBottom: S.xl }}>
|
|
<SectionHeading label={l.about} title="KLZ Cables" />
|
|
|
|
<RichText style={{ ...T.bodyLead, marginBottom: S.md }}>
|
|
{companyInfo.tagline}
|
|
</RichText>
|
|
|
|
{/* Inline image strip */}
|
|
{galleryImages?.[1] && <SectionImage src={galleryImages[1]} height={90} />}
|
|
|
|
{/* Values in 2-col grid */}
|
|
<Text style={{ ...T.label, marginBottom: S.sm }}>{l.values}</Text>
|
|
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: S.sm }}>
|
|
{companyInfo.values.map((v, i) => (
|
|
<View key={i} style={{ width: '48%', marginBottom: S.xs, flexDirection: 'row', alignItems: 'flex-start', gap: S.sm }} minPresenceAhead={40}>
|
|
<View style={{
|
|
width: 20, height: 20, borderRadius: 10,
|
|
backgroundColor: C.accent,
|
|
justifyContent: 'center', alignItems: 'center', flexShrink: 0, marginTop: 2,
|
|
}}>
|
|
<Text style={{ fontSize: 9, fontWeight: 700, color: C.white, textAlign: 'center' }}>0{i + 1}</Text>
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={{ ...T.bodyBold, marginBottom: 1 }}>{v.title}</Text>
|
|
<Text style={{ ...T.body, fontSize: 9 }}>{v.description}</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
<Divider />
|
|
|
|
{/* ── Marketing Sections ── */}
|
|
{marketingSections?.map((section, sIdx) => (
|
|
<View key={`m-${sIdx}`} style={{ marginBottom: S.xl }} minPresenceAhead={200}>
|
|
<SectionHeading label={section.subtitle} title={section.title} />
|
|
|
|
{/* Description */}
|
|
{section.description && (
|
|
<RichText style={{ ...T.bodyLead }} paragraphGap={S.sm}>
|
|
{section.description}
|
|
</RichText>
|
|
)}
|
|
|
|
{/* Pull quote */}
|
|
{section.pullQuote && <PullQuote quote={section.pullQuote} />}
|
|
|
|
{/* Stat highlights */}
|
|
{section.highlights && section.highlights.length > 0 && (
|
|
<HighlightRow highlights={section.highlights} />
|
|
)}
|
|
|
|
{/* Inline image for visual break (different image per section) */}
|
|
{galleryImages && galleryImages[sIdx + 2] && sIdx < 3 && (
|
|
<SectionImage src={galleryImages[sIdx + 2]} height={80} />
|
|
)}
|
|
|
|
{/* Item list */}
|
|
{section.items && (
|
|
<View style={{ flexDirection: 'column', gap: S.md, marginTop: S.sm }}>
|
|
{section.items.map((item, i) => (
|
|
<View key={i} style={{ flexDirection: 'row', alignItems: 'flex-start', gap: S.sm }} minPresenceAhead={60}>
|
|
<View style={{ flexShrink: 0, marginTop: 5 }}>
|
|
<View style={{ width: 5, height: 5, borderRadius: 2.5, backgroundColor: C.accent }} />
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={{ ...T.bodyBold, marginBottom: 2 }}>{item.title}</Text>
|
|
<Text style={{ ...T.body }}>{item.description}</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{/* Divider between sections */}
|
|
{sIdx < (marketingSections?.length || 0) - 1 && <Divider />}
|
|
</View>
|
|
))}
|
|
</View>
|
|
</Page>
|
|
);
|
|
};
|
|
|
|
// ─── TOC Page ───────────────────────────────────────────────────────────────
|
|
|
|
const TocPage: React.FC<{
|
|
products: BrochureProduct[];
|
|
locale: 'en' | 'de';
|
|
logoBlack?: string | Buffer;
|
|
productStartPage: number;
|
|
galleryImages?: Array<string | Buffer>;
|
|
}> = ({ products, locale, logoBlack, productStartPage, galleryImages }) => {
|
|
const l = L(locale);
|
|
|
|
const grouped = new Map<string, Array<{ product: BrochureProduct, pageNum: number }>>();
|
|
let currentGlobalIdx = 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 + currentGlobalIdx });
|
|
currentGlobalIdx++;
|
|
}
|
|
|
|
return (
|
|
<Page size="A4" style={{ backgroundColor: C.white, fontFamily: 'Helvetica', paddingTop: PAGE_TOP_PADDING, paddingBottom: M.bottom }}>
|
|
<FixedHeader logoBlack={logoBlack} rightText={l.overview} />
|
|
<Footer left="KLZ Cables" right="www.klz-cables.com" logoBlack={logoBlack} />
|
|
|
|
<View style={{ paddingHorizontal: M.h }}>
|
|
<SectionHeading label={l.catalog} title={l.toc} />
|
|
|
|
{/* Optional decorative image */}
|
|
{galleryImages?.[5] && <SectionImage src={galleryImages[5]} height={70} />}
|
|
|
|
<View style={{ flexDirection: 'column' }}>
|
|
{Array.from(grouped.entries()).map(([cat, items]) => (
|
|
<View key={cat} style={{ width: '100%', marginBottom: S.md }}>
|
|
<View style={{ borderBottomWidth: 1, borderBottomColor: C.accent, borderBottomStyle: 'solid', paddingBottom: S.xs, marginBottom: S.sm }}>
|
|
<Text style={{ ...T.label }}>{cat}</Text>
|
|
</View>
|
|
{items.map((item, i) => (
|
|
<View key={i} style={{
|
|
flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between',
|
|
paddingVertical: 3,
|
|
borderBottomWidth: i < items.length - 1 ? 0.5 : 0,
|
|
borderBottomColor: C.gray200, borderBottomStyle: 'solid',
|
|
}}>
|
|
<Text style={{ fontSize: 10, fontWeight: 700, color: C.primaryDark }}>{item.product.name}</Text>
|
|
<Text style={{ fontSize: 9, color: C.gray400 }}>{l.page} {item.pageNum}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
</Page>
|
|
);
|
|
};
|
|
|
|
// ─── Product Block ──────────────────────────────────────────────────────────
|
|
|
|
const ProductBlock: React.FC<{
|
|
product: BrochureProduct;
|
|
locale: 'en' | 'de';
|
|
}> = ({ product, locale }) => {
|
|
const l = L(locale);
|
|
const desc = stripHtml(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml);
|
|
|
|
return (
|
|
<View>
|
|
<SectionHeading
|
|
label={product.categories.map(c => c.name).join(' · ')}
|
|
title={product.name}
|
|
/>
|
|
|
|
{/* Full-width product image */}
|
|
<View style={{
|
|
width: '100%', height: 120, justifyContent: 'center', alignItems: 'center',
|
|
borderRadius: 4, backgroundColor: C.gray050,
|
|
borderWidth: 0.5, borderColor: C.gray200, borderStyle: 'solid',
|
|
overflow: 'hidden', padding: S.sm, marginBottom: S.lg,
|
|
}}>
|
|
{product.featuredImage ? (
|
|
<Image src={product.featuredImage} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
|
) : (
|
|
<Text style={{ fontSize: 8, color: C.gray400 }}>—</Text>
|
|
)}
|
|
</View>
|
|
|
|
{/* Description + QR */}
|
|
<View style={{ flexDirection: 'row', gap: S.xl, marginBottom: S.lg }}>
|
|
<View style={{ flex: 1.6 }}>
|
|
{desc && (
|
|
<View>
|
|
<Text style={{ ...T.label, marginBottom: S.sm }}>{l.application}</Text>
|
|
<RichText style={{ ...T.body, lineHeight: 1.8 }}>{desc}</RichText>
|
|
</View>
|
|
)}
|
|
</View>
|
|
<View style={{ flex: 1, flexDirection: 'column', justifyContent: 'flex-start' }}>
|
|
{(product.qrWebsite || product.qrDatasheet) && (
|
|
<View style={{ flexDirection: 'column', gap: S.sm }}>
|
|
{product.qrWebsite && (
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: S.sm }}>
|
|
<Image src={product.qrWebsite} style={{ width: 32, height: 32 }} />
|
|
<View>
|
|
<Text style={{ ...T.caption, fontWeight: 700, color: C.primaryDark, marginBottom: 1 }}>{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: S.sm }}>
|
|
<Image src={product.qrDatasheet} style={{ width: 32, height: 32 }} />
|
|
<View>
|
|
<Text style={{ ...T.caption, fontWeight: 700, color: C.primaryDark, marginBottom: 1 }}>{l.qrPdf}</Text>
|
|
<Text style={{ fontSize: 7, color: C.gray400 }}>{locale === 'de' ? 'Datenblatt' : 'Datasheet'}</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Technical Data Table — clean, minimal header */}
|
|
{product.attributes && product.attributes.length > 0 && (
|
|
<View>
|
|
<Text style={{ ...T.label, marginBottom: S.sm }}>{l.specs}</Text>
|
|
<View style={{
|
|
borderWidth: 0.5, borderColor: C.gray200, borderStyle: 'solid',
|
|
borderRadius: 4, overflow: 'hidden',
|
|
}}>
|
|
{/* Subtle header */}
|
|
<View style={{
|
|
flexDirection: 'row',
|
|
backgroundColor: C.gray050,
|
|
borderBottomWidth: 1, borderBottomColor: C.accent, borderBottomStyle: 'solid',
|
|
}}>
|
|
<View style={{
|
|
width: 200, paddingVertical: S.xs, paddingHorizontal: 10,
|
|
borderRightWidth: 0.5, borderRightColor: C.gray200, borderRightStyle: 'solid'
|
|
}}>
|
|
<Text style={{ ...T.label, fontSize: 7, color: C.gray600 }}>
|
|
{locale === 'de' ? 'Eigenschaft' : 'Property'}
|
|
</Text>
|
|
</View>
|
|
<View style={{ flex: 1, paddingVertical: S.xs, paddingHorizontal: 10 }}>
|
|
<Text style={{ ...T.label, fontSize: 7, color: C.gray600 }}>
|
|
{locale === 'de' ? 'Wert' : 'Value'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Rows */}
|
|
{product.attributes.map((attr, i) => (
|
|
<View key={i} style={{
|
|
flexDirection: 'row',
|
|
borderBottomWidth: i < product.attributes.length - 1 ? 0.5 : 0,
|
|
borderBottomColor: C.gray200, borderBottomStyle: 'solid',
|
|
backgroundColor: i % 2 === 0 ? C.white : C.gray050,
|
|
}}>
|
|
<View style={{
|
|
width: 200, paddingVertical: S.xs, paddingHorizontal: 10,
|
|
borderRightWidth: 0.5, borderRightColor: C.gray200, borderRightStyle: 'solid',
|
|
}}>
|
|
<Text style={{ fontSize: 8, fontWeight: 700, color: C.primaryDark, textTransform: 'uppercase', letterSpacing: 0.3 }}>{attr.name}</Text>
|
|
</View>
|
|
<View style={{ flex: 1, paddingVertical: S.xs, paddingHorizontal: 10, justifyContent: 'center' }}>
|
|
<Text style={{ fontSize: 9, color: C.gray900 }}>{attr.options.join(', ')}</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// ─── Products Flow ──────────────────────────────────────────────────────────
|
|
|
|
const ProductsFlow: React.FC<{
|
|
products: BrochureProduct[];
|
|
locale: 'en' | 'de';
|
|
logoBlack?: string | Buffer;
|
|
}> = ({ products, locale, logoBlack }) => {
|
|
const l = L(locale);
|
|
return (
|
|
<React.Fragment>
|
|
{products.map((p) => (
|
|
<Page key={p.id} size="A4" style={{ backgroundColor: C.white, fontFamily: 'Helvetica', paddingTop: PAGE_TOP_PADDING, paddingBottom: M.bottom }}>
|
|
<FixedHeader logoBlack={logoBlack} rightText={l.overview} />
|
|
<Footer left="KLZ Cables" right="www.klz-cables.com" logoBlack={logoBlack} />
|
|
<View style={{ paddingHorizontal: M.h }}>
|
|
<ProductBlock product={p} locale={locale} />
|
|
</View>
|
|
</Page>
|
|
))}
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
// ─── Back Cover ─────────────────────────────────────────────────────────────
|
|
|
|
const BackCoverPage: React.FC<{
|
|
companyInfo: BrochureProps['companyInfo'];
|
|
locale: 'en' | 'de';
|
|
logoBlack?: string | Buffer;
|
|
galleryImages?: Array<string | Buffer>;
|
|
}> = ({ companyInfo, locale, logoBlack, galleryImages }) => {
|
|
const l = L(locale);
|
|
const bgImage = galleryImages?.[6];
|
|
|
|
return (
|
|
<Page size="A4" style={{ backgroundColor: C.white, fontFamily: 'Helvetica' }}>
|
|
{bgImage && (
|
|
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
|
|
<Image src={bgImage} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.white, opacity: 0.9 }} />
|
|
</View>
|
|
)}
|
|
|
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: M.h }}>
|
|
{logoBlack ? (
|
|
<Image src={logoBlack} style={{ width: 180, marginBottom: S.xl }} />
|
|
) : (
|
|
<Text style={{ fontSize: 32, fontWeight: 700, color: C.black, letterSpacing: 2, textTransform: 'uppercase', marginBottom: S.xl }}>KLZ CABLES</Text>
|
|
)}
|
|
|
|
<View style={{ width: 48, height: 4, backgroundColor: C.accent, borderRadius: 2, marginBottom: S.xl }} />
|
|
|
|
<View style={{ alignItems: 'center', marginBottom: S.lg }}>
|
|
<Text style={{ ...T.label, marginBottom: S.sm }}>{l.contact}</Text>
|
|
<Text style={{ fontSize: 13, color: C.gray900, lineHeight: 1.7, textAlign: 'center' }}>{companyInfo.address}</Text>
|
|
</View>
|
|
|
|
<View style={{ alignItems: 'center', marginBottom: S.lg }}>
|
|
<Text style={{ fontSize: 13, color: C.gray900 }}>{companyInfo.phone}</Text>
|
|
<Text style={{ fontSize: 13, color: C.gray900 }}>{companyInfo.email}</Text>
|
|
</View>
|
|
|
|
<Text style={{ fontSize: 14, fontWeight: 700, color: C.accent, marginTop: S.lg }}>{companyInfo.website}</Text>
|
|
</View>
|
|
|
|
<View style={{ position: 'absolute', bottom: 40, left: M.h, right: M.h, flexDirection: 'row', justifyContent: 'center' }} fixed>
|
|
<Text style={{ fontSize: 9, color: C.gray400 }}>© {new Date().getFullYear()} KLZ Cables GmbH</Text>
|
|
</View>
|
|
</Page>
|
|
);
|
|
};
|
|
|
|
// ─── Main Document ──────────────────────────────────────────────────────────
|
|
|
|
export const PDFBrochure: React.FC<BrochureProps> = ({
|
|
products, locale, companyInfo, introContent,
|
|
marketingSections, logoBlack, galleryImages,
|
|
}) => {
|
|
const productStartPage = 5;
|
|
|
|
return (
|
|
<Document>
|
|
<CoverPage locale={locale} introContent={introContent} logoBlack={logoBlack} galleryImages={galleryImages} />
|
|
<InfoFlow locale={locale} companyInfo={companyInfo} introContent={introContent}
|
|
marketingSections={marketingSections} logoBlack={logoBlack}
|
|
galleryImages={galleryImages} />
|
|
<TocPage products={products} locale={locale} logoBlack={logoBlack}
|
|
productStartPage={productStartPage} galleryImages={galleryImages} />
|
|
<ProductsFlow products={products} locale={locale} logoBlack={logoBlack} />
|
|
<BackCoverPage companyInfo={companyInfo} locale={locale} logoBlack={logoBlack} galleryImages={galleryImages} />
|
|
</Document>
|
|
);
|
|
};
|