feat(brochure): implement multi-product PDF brochure generation system
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
This commit is contained in:
@@ -496,8 +496,12 @@ jobs:
|
|||||||
curl -sf "$DEPLOY_URL/health" || { echo "❌ Basic health check failed"; exit 1; }
|
curl -sf "$DEPLOY_URL/health" || { echo "❌ Basic health check failed"; exit 1; }
|
||||||
echo "✅ Basic health OK"
|
echo "✅ Basic health OK"
|
||||||
echo "Checking CMS DB connectivity..."
|
echo "Checking CMS DB connectivity..."
|
||||||
|
RESPONSE_HEADERS=$(curl -sfI "$DEPLOY_URL/api/health/cms?gk_bypass=$GK_PASS" 2>&1)
|
||||||
RESPONSE=$(curl -sf "$DEPLOY_URL/api/health/cms?gk_bypass=$GK_PASS" 2>&1) || {
|
RESPONSE=$(curl -sf "$DEPLOY_URL/api/health/cms?gk_bypass=$GK_PASS" 2>&1) || {
|
||||||
echo "❌ CMS health check failed!"
|
echo "❌ CMS health check failed!"
|
||||||
|
echo "--- RESPONSE HEADERS ---"
|
||||||
|
curl -sI "$DEPLOY_URL/api/health/cms?gk_bypass=$GK_PASS"
|
||||||
|
echo "--- RESPONSE BODY ---"
|
||||||
echo "$RESPONSE"
|
echo "$RESPONSE"
|
||||||
echo ""
|
echo ""
|
||||||
echo "This usually means Payload CMS migrations failed or DB tables are missing."
|
echo "This usually means Payload CMS migrations failed or DB tables are missing."
|
||||||
|
|||||||
84
app/actions/brochure.ts
Normal file
84
app/actions/brochure.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { renderToBuffer } from '@react-pdf/renderer';
|
||||||
|
import { getAllProducts } from '@/lib/products';
|
||||||
|
import { getExcelTechnicalDataForProduct } from '@/lib/excel-products';
|
||||||
|
import { PDFBrochure } from '@/lib/pdf-brochure';
|
||||||
|
import { ProductData as PDFProductData } from '@/lib/pdf-datasheet';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server action to generate a PDF brochure.
|
||||||
|
* Fetches products from Payload CMS and technical data from Excel.
|
||||||
|
* Returns the PDF as a base64 string.
|
||||||
|
*/
|
||||||
|
export async function generateBrochureAction(params: {
|
||||||
|
locale: 'en' | 'de';
|
||||||
|
productSlugs?: string[];
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const { locale, productSlugs, title, subtitle } = params;
|
||||||
|
|
||||||
|
// 1. Fetch products from Payload
|
||||||
|
let products = await getAllProducts(locale);
|
||||||
|
|
||||||
|
// 2. Filter by slugs if provided
|
||||||
|
if (productSlugs && productSlugs.length > 0) {
|
||||||
|
products = products.filter((p) => productSlugs.includes(p.slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Map Payload products to PDF internal model and attach Excel data
|
||||||
|
const pdfProducts: PDFProductData[] = products.map((p, index) => {
|
||||||
|
// Get technical data from Excel
|
||||||
|
const excelData = getExcelTechnicalDataForProduct({
|
||||||
|
slug: p.slug,
|
||||||
|
sku: p.frontmatter.sku,
|
||||||
|
name: p.frontmatter.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: index + 1,
|
||||||
|
name: p.frontmatter.title,
|
||||||
|
sku: p.frontmatter.sku,
|
||||||
|
shortDescriptionHtml: p.frontmatter.description,
|
||||||
|
descriptionHtml: p.frontmatter.description,
|
||||||
|
// Application text is usually part of description in Payload v3
|
||||||
|
applicationHtml: '',
|
||||||
|
featuredImage: p.frontmatter.images[0] || null,
|
||||||
|
images: p.frontmatter.images,
|
||||||
|
categories: p.frontmatter.categories.map((name) => ({ name })),
|
||||||
|
attributes: excelData?.attributes || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pdfProducts.length === 0) {
|
||||||
|
throw new Error('No products found for brochure generation.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Render to PDF buffer
|
||||||
|
// Note: React 19 / Next 15+ might need specific handling for renderToBuffer in actions
|
||||||
|
const buffer = await renderToBuffer(
|
||||||
|
React.createElement(PDFBrochure, {
|
||||||
|
products: pdfProducts,
|
||||||
|
locale,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Convert to Base64 for returning to client
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: buffer.toString('base64'),
|
||||||
|
fileName: `KLZ_Cables_Brochure_${locale.toUpperCase()}.pdf`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Action] Brochure generation failed:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
95
lib/pdf-brochure.tsx
Normal file
95
lib/pdf-brochure.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Document, Page, View, Text, StyleSheet, Font, Image } from '@react-pdf/renderer';
|
||||||
|
import { ProductData, ProductDatasheetPage } from './pdf-datasheet';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
cover: {
|
||||||
|
backgroundColor: '#001a4d', // Navy
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
padding: 40,
|
||||||
|
},
|
||||||
|
logoContainer: {
|
||||||
|
marginBottom: 40,
|
||||||
|
},
|
||||||
|
logoText: {
|
||||||
|
fontSize: 64,
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: 4,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: 700,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 2,
|
||||||
|
marginBottom: 20,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#82ed20', // Accent Green
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 4,
|
||||||
|
marginBottom: 60,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 60,
|
||||||
|
fontSize: 10,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: '#e5e7eb',
|
||||||
|
},
|
||||||
|
accentBar: {
|
||||||
|
width: 60,
|
||||||
|
height: 4,
|
||||||
|
backgroundColor: '#82ed20',
|
||||||
|
marginBottom: 40,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PDFBrochureProps {
|
||||||
|
products: ProductData[];
|
||||||
|
locale: 'en' | 'de';
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PDFBrochure: React.FC<PDFBrochureProps> = ({
|
||||||
|
products,
|
||||||
|
locale,
|
||||||
|
title = locale === 'de' ? 'Produktkatalog' : 'Product Catalog',
|
||||||
|
subtitle = '2026',
|
||||||
|
}) => {
|
||||||
|
const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Document>
|
||||||
|
{/* Cover Page */}
|
||||||
|
<Page size="A4" style={styles.cover}>
|
||||||
|
<View style={styles.logoContainer}>
|
||||||
|
<Text style={styles.logoText}>KLZ</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.accentBar} />
|
||||||
|
<Text style={styles.title}>{title}</Text>
|
||||||
|
<Text style={styles.subtitle}>{subtitle}</Text>
|
||||||
|
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<Text>{dateStr} — KLZ CABLES</Text>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
|
||||||
|
{/* Product Pages */}
|
||||||
|
{products.map((product) => (
|
||||||
|
<ProductDatasheetPage key={product.id} product={product} locale={locale} />
|
||||||
|
))}
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -238,7 +238,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ProductData {
|
export interface ProductData {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
shortDescriptionHtml: string;
|
shortDescriptionHtml: string;
|
||||||
@@ -254,6 +254,123 @@ interface ProductData {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProductDatasheetPageProps {
|
||||||
|
product: ProductData;
|
||||||
|
locale: 'en' | 'de';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductDatasheetPage: React.FC<ProductDatasheetPageProps> = ({
|
||||||
|
product,
|
||||||
|
locale,
|
||||||
|
}) => {
|
||||||
|
const labels = getLabels(locale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page size="A4" style={styles.page}>
|
||||||
|
{/* Hero Header */}
|
||||||
|
<View style={styles.hero}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.logoText}>KLZ</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.docTitle}>{labels.productDatasheet}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.productRow}>
|
||||||
|
<View style={styles.productInfoCol}>
|
||||||
|
<View style={styles.productHero}>
|
||||||
|
<View style={styles.categories}>
|
||||||
|
{product.categories.map((cat, index) => (
|
||||||
|
<Text key={index} style={styles.productMeta}>
|
||||||
|
{cat.name}
|
||||||
|
{index < product.categories.length - 1 ? ' • ' : ''}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.productName}>{product.name}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.productImageCol}>
|
||||||
|
{product.featuredImage ? (
|
||||||
|
<Image src={product.featuredImage} style={styles.heroImage} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.noImage}>{labels.noImage}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.content}>
|
||||||
|
{/* Description section */}
|
||||||
|
{(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>{labels.description}</Text>
|
||||||
|
<View style={styles.sectionAccent} />
|
||||||
|
<Text style={styles.description}>
|
||||||
|
{stripHtml(
|
||||||
|
product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml,
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Technical specifications */}
|
||||||
|
{product.attributes && product.attributes.length > 0 && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
|
||||||
|
<View style={styles.sectionAccent} />
|
||||||
|
<View style={styles.specsTable}>
|
||||||
|
{product.attributes.map((attr, index) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
styles.specsTableRow,
|
||||||
|
index === product.attributes.length - 1 && styles.specsTableRowLast,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.specsTableLabelCell}>
|
||||||
|
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.specsTableValueCell}>
|
||||||
|
<Text style={styles.specsTableValueText}>{attr.options.join(', ')}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Categories as clean tags */}
|
||||||
|
{product.categories && product.categories.length > 0 && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>{labels.categories}</Text>
|
||||||
|
<View style={styles.sectionAccent} />
|
||||||
|
<View style={styles.categories}>
|
||||||
|
{product.categories.map((cat, index) => (
|
||||||
|
<View key={index} style={styles.categoryTag}>
|
||||||
|
<Text style={styles.categoryText}>{cat.name}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Minimal footer */}
|
||||||
|
<View style={styles.footer} fixed>
|
||||||
|
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
||||||
|
<Text style={styles.footerText}>
|
||||||
|
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface PDFDatasheetProps {
|
interface PDFDatasheetProps {
|
||||||
product: ProductData;
|
product: ProductData;
|
||||||
locale: 'en' | 'de';
|
locale: 'en' | 'de';
|
||||||
@@ -289,114 +406,10 @@ const getLabels = (locale: 'en' | 'de') => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ product, locale }) => {
|
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ product, locale }) => {
|
||||||
const labels = getLabels(locale);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page size="A4" style={styles.page}>
|
<ProductDatasheetPage product={product} locale={locale} />
|
||||||
{/* Hero Header */}
|
|
||||||
<View style={styles.hero}>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<View>
|
|
||||||
<Text style={styles.logoText}>KLZ</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.docTitle}>{labels.productDatasheet}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.productRow}>
|
|
||||||
<View style={styles.productInfoCol}>
|
|
||||||
<View style={styles.productHero}>
|
|
||||||
<View style={styles.categories}>
|
|
||||||
{product.categories.map((cat, index) => (
|
|
||||||
<Text key={index} style={styles.productMeta}>
|
|
||||||
{cat.name}
|
|
||||||
{index < product.categories.length - 1 ? ' • ' : ''}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
<Text style={styles.productName}>{product.name}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.productImageCol}>
|
|
||||||
{product.featuredImage ? (
|
|
||||||
<Image src={product.featuredImage} style={styles.heroImage} />
|
|
||||||
) : (
|
|
||||||
<Text style={styles.noImage}>{labels.noImage}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
|
||||||
{/* Description section */}
|
|
||||||
{(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && (
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>{labels.description}</Text>
|
|
||||||
<View style={styles.sectionAccent} />
|
|
||||||
<Text style={styles.description}>
|
|
||||||
{stripHtml(
|
|
||||||
product.applicationHtml ||
|
|
||||||
product.shortDescriptionHtml ||
|
|
||||||
product.descriptionHtml,
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Technical specifications */}
|
|
||||||
{product.attributes && product.attributes.length > 0 && (
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
|
|
||||||
<View style={styles.sectionAccent} />
|
|
||||||
<View style={styles.specsTable}>
|
|
||||||
{product.attributes.map((attr, index) => (
|
|
||||||
<View
|
|
||||||
key={index}
|
|
||||||
style={[
|
|
||||||
styles.specsTableRow,
|
|
||||||
index === product.attributes.length - 1 && styles.specsTableRowLast,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={styles.specsTableLabelCell}>
|
|
||||||
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.specsTableValueCell}>
|
|
||||||
<Text style={styles.specsTableValueText}>{attr.options.join(', ')}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Categories as clean tags */}
|
|
||||||
{product.categories && product.categories.length > 0 && (
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>{labels.categories}</Text>
|
|
||||||
<View style={styles.sectionAccent} />
|
|
||||||
<View style={styles.categories}>
|
|
||||||
{product.categories.map((cat, index) => (
|
|
||||||
<View key={index} style={styles.categoryTag}>
|
|
||||||
<Text style={styles.categoryText}>{cat.name}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Minimal footer */}
|
|
||||||
<View style={styles.footer} fixed>
|
|
||||||
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
|
||||||
<Text style={styles.footerText}>
|
|
||||||
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Page>
|
|
||||||
</Document>
|
</Document>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
102
scripts/generate-brochure.ts
Normal file
102
scripts/generate-brochure.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env ts-node
|
||||||
|
/**
|
||||||
|
* Master Brochure Generator
|
||||||
|
*
|
||||||
|
* Generates a multi-page PDF brochure containing all products.
|
||||||
|
* Combines Payload CMS marketing text with Excel technical data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { renderToBuffer } from '@react-pdf/renderer';
|
||||||
|
import { getAllProducts } from '../lib/products';
|
||||||
|
import { getExcelTechnicalDataForProduct, preloadExcelData } from '../lib/excel-products';
|
||||||
|
import { PDFBrochure } from '../lib/pdf-brochure';
|
||||||
|
import { ProductData as PDFProductData } from '../lib/pdf-datasheet';
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
outputDir: path.join(process.cwd(), 'public/downloads'),
|
||||||
|
locales: ['en', 'de'] as const,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
async function ensureOutputDir() {
|
||||||
|
if (!fs.existsSync(CONFIG.outputDir)) {
|
||||||
|
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateBrochure(locale: 'en' | 'de') {
|
||||||
|
console.log(`\nGenerating brochure for locale: ${locale.toUpperCase()}...`);
|
||||||
|
|
||||||
|
// 1. Load data
|
||||||
|
const products = await getAllProducts(locale);
|
||||||
|
console.log(`- Loaded ${products.length} products from Payload`);
|
||||||
|
|
||||||
|
// 2. Map and Enrich
|
||||||
|
const pdfProducts: PDFProductData[] = products.map((p, index) => {
|
||||||
|
const excelData = getExcelTechnicalDataForProduct({
|
||||||
|
slug: p.slug,
|
||||||
|
sku: p.frontmatter.sku,
|
||||||
|
name: p.frontmatter.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: index + 1,
|
||||||
|
name: p.frontmatter.title,
|
||||||
|
sku: p.frontmatter.sku,
|
||||||
|
shortDescriptionHtml: p.frontmatter.description,
|
||||||
|
descriptionHtml: p.frontmatter.description,
|
||||||
|
applicationHtml: '',
|
||||||
|
featuredImage: p.frontmatter.images[0] || null,
|
||||||
|
images: p.frontmatter.images,
|
||||||
|
categories: p.frontmatter.categories.map(name => ({ name })),
|
||||||
|
attributes: excelData?.attributes || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pdfProducts.length === 0) {
|
||||||
|
console.warn(`! No products found for ${locale}. Skipping.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Render
|
||||||
|
console.log(`- Rendering PDF with ${pdfProducts.length} pages...`);
|
||||||
|
const buffer = await renderToBuffer(
|
||||||
|
React.createElement(PDFBrochure, {
|
||||||
|
products: pdfProducts,
|
||||||
|
locale,
|
||||||
|
title: locale === 'de' ? 'Produktkatalog' : 'Product Catalog',
|
||||||
|
subtitle: '2026',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Save
|
||||||
|
const fileName = `KLZ_Cables_Brochure_2026_${locale.toUpperCase()}.pdf`;
|
||||||
|
const filePath = path.join(CONFIG.outputDir, fileName);
|
||||||
|
fs.writeFileSync(filePath, buffer);
|
||||||
|
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
console.log(`✓ Generated: ${fileName} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const start = Date.now();
|
||||||
|
console.log('--- KLZ Brochure Generator ---');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ensureOutputDir();
|
||||||
|
preloadExcelData();
|
||||||
|
|
||||||
|
for (const locale of CONFIG.locales) {
|
||||||
|
await generateBrochure(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✅ Brochure generation completed in ${((Date.now() - start) / 1000).toFixed(2)}s`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
Reference in New Issue
Block a user