diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index df19b7e7..efec5709 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -496,8 +496,12 @@ jobs: curl -sf "$DEPLOY_URL/health" || { echo "❌ Basic health check failed"; exit 1; } echo "✅ Basic health OK" 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) || { 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 "" echo "This usually means Payload CMS migrations failed or DB tables are missing." diff --git a/app/actions/brochure.ts b/app/actions/brochure.ts new file mode 100644 index 00000000..ad6a0a21 --- /dev/null +++ b/app/actions/brochure.ts @@ -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', + }; + } +} diff --git a/lib/pdf-brochure.tsx b/lib/pdf-brochure.tsx new file mode 100644 index 00000000..e3c27c23 --- /dev/null +++ b/lib/pdf-brochure.tsx @@ -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 = ({ + 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 ( + + {/* Cover Page */} + + + KLZ + + + {title} + {subtitle} + + + {dateStr} — KLZ CABLES + + + + {/* Product Pages */} + {products.map((product) => ( + + ))} + + ); +}; diff --git a/lib/pdf-datasheet.tsx b/lib/pdf-datasheet.tsx index ae7dc787..015494dc 100644 --- a/lib/pdf-datasheet.tsx +++ b/lib/pdf-datasheet.tsx @@ -238,7 +238,7 @@ const styles = StyleSheet.create({ }, }); -interface ProductData { +export interface ProductData { id: number; name: string; shortDescriptionHtml: string; @@ -254,6 +254,123 @@ interface ProductData { }>; } +interface ProductDatasheetPageProps { + product: ProductData; + locale: 'en' | 'de'; +} + +export const ProductDatasheetPage: React.FC = ({ + product, + locale, +}) => { + const labels = getLabels(locale); + + return ( + + {/* Hero Header */} + + + + KLZ + + {labels.productDatasheet} + + + + + + + {product.categories.map((cat, index) => ( + + {cat.name} + {index < product.categories.length - 1 ? ' • ' : ''} + + ))} + + {product.name} + + + + {product.featuredImage ? ( + + ) : ( + {labels.noImage} + )} + + + + + + {/* Description section */} + {(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && ( + + {labels.description} + + + {stripHtml( + product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml, + )} + + + )} + + {/* Technical specifications */} + {product.attributes && product.attributes.length > 0 && ( + + {labels.specifications} + + + {product.attributes.map((attr, index) => ( + + + {attr.name} + + + {attr.options.join(', ')} + + + ))} + + + )} + + {/* Categories as clean tags */} + {product.categories && product.categories.length > 0 && ( + + {labels.categories} + + + {product.categories.map((cat, index) => ( + + {cat.name} + + ))} + + + )} + + + {/* Minimal footer */} + + KLZ CABLES + + {new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + + + + ); +}; + interface PDFDatasheetProps { product: ProductData; locale: 'en' | 'de'; @@ -289,114 +406,10 @@ const getLabels = (locale: 'en' | 'de') => { }; export const PDFDatasheet: React.FC = ({ product, locale }) => { - const labels = getLabels(locale); - return ( - - {/* Hero Header */} - - - - KLZ - - {labels.productDatasheet} - - - - - - - {product.categories.map((cat, index) => ( - - {cat.name} - {index < product.categories.length - 1 ? ' • ' : ''} - - ))} - - {product.name} - - - - {product.featuredImage ? ( - - ) : ( - {labels.noImage} - )} - - - - - - {/* Description section */} - {(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && ( - - {labels.description} - - - {stripHtml( - product.applicationHtml || - product.shortDescriptionHtml || - product.descriptionHtml, - )} - - - )} - - {/* Technical specifications */} - {product.attributes && product.attributes.length > 0 && ( - - {labels.specifications} - - - {product.attributes.map((attr, index) => ( - - - {attr.name} - - - {attr.options.join(', ')} - - - ))} - - - )} - - {/* Categories as clean tags */} - {product.categories && product.categories.length > 0 && ( - - {labels.categories} - - - {product.categories.map((cat, index) => ( - - {cat.name} - - ))} - - - )} - - - {/* Minimal footer */} - - KLZ CABLES - - {new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', { - year: 'numeric', - month: 'long', - day: 'numeric', - })} - - - + ); }; + diff --git a/scripts/generate-brochure.ts b/scripts/generate-brochure.ts new file mode 100644 index 00000000..fc2acf08 --- /dev/null +++ b/scripts/generate-brochure.ts @@ -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);