feat: product catalog
This commit is contained in:
@@ -254,91 +254,63 @@ async function loadMarketingSections(locale: 'en' | 'de'): Promise<BrochureProps
|
||||
|
||||
const sections: NonNullable<BrochureProps['marketingSections']> = [];
|
||||
|
||||
// 1. What we do — short label, long subtitle becomes the description
|
||||
if (messages.Home?.whatWeDo) {
|
||||
const label = locale === 'de' ? 'Unser Angebot' : 'Our Services';
|
||||
sections.push({
|
||||
title: messages.Home.whatWeDo.title,
|
||||
subtitle: label,
|
||||
description: messages.Home.whatWeDo.subtitle,
|
||||
items: messages.Home.whatWeDo.items,
|
||||
});
|
||||
}
|
||||
// ── 1. Was wir tun + Warum wir — MERGED into one compact section ──
|
||||
{
|
||||
const allItems: Array<{ title: string; description: string }> = [];
|
||||
|
||||
// 2. Our Legacy — with stats highlight
|
||||
if (messages.Team?.legacy) {
|
||||
const label = locale === 'de' ? 'Unsere Geschichte' : 'Our Story';
|
||||
sections.push({
|
||||
title: messages.Team.legacy.title,
|
||||
subtitle: label,
|
||||
description: `${messages.Team.legacy.p1}\n\n${messages.Team.legacy.p2}`,
|
||||
highlights: [
|
||||
{ value: messages.Team.legacy.expertise || 'Expertise', label: messages.Team.legacy.expertiseDesc || '' },
|
||||
{ value: messages.Team.legacy.network || 'Netzwerk', label: messages.Team.legacy.networkDesc || '' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Experience stats section
|
||||
if (messages.Home?.experience) {
|
||||
const label = locale === 'de' ? 'Erfahrung' : 'Experience';
|
||||
sections.push({
|
||||
title: messages.Home.experience.title,
|
||||
subtitle: label,
|
||||
description: `${messages.Home.experience.p1 || ''}\n\n${messages.Home.experience.p2 || ''}`.trim(),
|
||||
highlights: [
|
||||
{ value: messages.Home.experience.certifiedQuality || (locale === 'de' ? 'Zertifizierte Qualität' : 'Certified Quality'), label: messages.Home.experience.vdeApproved || '' },
|
||||
{ value: messages.Home.experience.fullSpectrum || (locale === 'de' ? 'Volles Spektrum' : 'Full Spectrum'), label: messages.Home.experience.solutionsRange || '' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Why choose us
|
||||
if (messages.Home?.whyChooseUs) {
|
||||
const label = locale === 'de' ? 'Warum KLZ' : 'Why KLZ';
|
||||
sections.push({
|
||||
title: messages.Home.whyChooseUs.title,
|
||||
subtitle: label,
|
||||
description: messages.Home.whyChooseUs.subtitle || '',
|
||||
items: messages.Home.whyChooseUs.items,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Team intro + quotes as pull quotes
|
||||
if (messages.Team?.klaus || messages.Team?.michael) {
|
||||
const label = locale === 'de' ? 'Die Geschäftsführer' : 'The Directors';
|
||||
const title = messages.Home?.meetTheTeam?.title || (locale === 'de' ? 'Das Team' : 'The Team');
|
||||
const teamItems: Array<{ title: string; description: string }> = [];
|
||||
if (messages.Team?.klaus) {
|
||||
teamItems.push({
|
||||
title: `${messages.Team.klaus.name} – ${messages.Team.klaus.role}`,
|
||||
description: messages.Team.klaus.description,
|
||||
});
|
||||
// WhatWeDo items — truncated to 1 sentence each
|
||||
if (messages.Home?.whatWeDo?.items) {
|
||||
for (const item of messages.Home.whatWeDo.items) {
|
||||
allItems.push({
|
||||
title: item.title.split('.')[0], // short title
|
||||
description: item.description.split('.')[0] + '.',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (messages.Team?.michael) {
|
||||
teamItems.push({
|
||||
title: `${messages.Team.michael.name} – ${messages.Team.michael.role}`,
|
||||
description: messages.Team.michael.description,
|
||||
});
|
||||
// WhyChooseUs items — truncated to 1 sentence each
|
||||
if (messages.Home?.whyChooseUs?.items) {
|
||||
for (const item of messages.Home.whyChooseUs.items) {
|
||||
allItems.push({
|
||||
title: item.title,
|
||||
description: item.description.split('.')[0] + '.',
|
||||
});
|
||||
}
|
||||
}
|
||||
const desc = messages.Home?.meetTheTeam?.description || '';
|
||||
|
||||
sections.push({
|
||||
title,
|
||||
subtitle: label,
|
||||
title: messages.Home?.whatWeDo?.title || (locale === 'de' ? 'Was wir tun' : 'What We Do'),
|
||||
subtitle: locale === 'de' ? 'Leistungen & Stärken' : 'Services & Strengths',
|
||||
description: messages.Home?.whatWeDo?.subtitle || '',
|
||||
items: allItems,
|
||||
});
|
||||
}
|
||||
|
||||
// ── 2. Experience & Quality — merge Legacy + Experience highlights ──
|
||||
{
|
||||
const legacy = messages.Team?.legacy;
|
||||
const experience = messages.Home?.experience;
|
||||
const highlights: Array<{ value: string; label: string }> = [];
|
||||
|
||||
if (legacy) {
|
||||
highlights.push(
|
||||
{ value: legacy.expertise || 'Expertise', label: legacy.expertiseDesc || '' },
|
||||
{ value: legacy.network || (locale === 'de' ? 'Netzwerk' : 'Network'), label: legacy.networkDesc || '' },
|
||||
);
|
||||
}
|
||||
if (experience) {
|
||||
highlights.push(
|
||||
{ value: experience.certifiedQuality || (locale === 'de' ? 'Zertifiziert' : 'Certified'), label: experience.vdeApproved || '' },
|
||||
{ value: experience.fullSpectrum || (locale === 'de' ? 'Volles Spektrum' : 'Full Spectrum'), label: experience.solutionsRange || '' },
|
||||
);
|
||||
}
|
||||
|
||||
const desc = legacy?.p1 || '';
|
||||
|
||||
sections.push({
|
||||
title: legacy?.title || (locale === 'de' ? 'Erfahrung & Qualität' : 'Experience & Quality'),
|
||||
subtitle: locale === 'de' ? 'Unser Erbe' : 'Our Heritage',
|
||||
description: desc,
|
||||
items: teamItems,
|
||||
pullQuote: messages.Team.klaus?.quote || messages.Team.michael?.quote || '',
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Our Values (Manifesto)
|
||||
if (messages.Team?.manifesto) {
|
||||
const label = locale === 'de' ? 'Grundprinzipien' : 'Core Principles';
|
||||
sections.push({
|
||||
title: messages.Team.manifesto.title,
|
||||
subtitle: label,
|
||||
description: messages.Team.manifesto.tagline,
|
||||
items: messages.Team.manifesto.items,
|
||||
highlights,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -395,18 +367,14 @@ async function main(): Promise<void> {
|
||||
|
||||
console.log(`Logos: white=${!!logoWhite} black=${!!logoBlack}`);
|
||||
|
||||
// EXACT image mapping to website sections to prevent "random" images.
|
||||
// Index map: 0=Cover, 1=About, 2=WhatWeDo, 3=Legacy, 4=Experience, 5=WhyChooseUs, 6=Team, 7=Manifesto, 8=BackCover
|
||||
// EXACT image mapping — 2 marketing sections now
|
||||
// Index map: 0=Cover, 1=About, 2=WasWirTun(null), 3=Erfahrung(Legacy image), 4=BackCover
|
||||
const galleryPaths: Array<string | null> = [
|
||||
'uploads/2024/12/DSC07655-Large.webp', // 0: Cover (Hero)
|
||||
'uploads/2024/12/large-rolls-of-wires-against-the-blue-sky-at-sunse-2023-11-27-05-20-33-utc-Large.webp', // 0: Cover (cable drums, no people)
|
||||
'uploads/2024/12/DSC07460-Large-600x400.webp', // 1: About section
|
||||
null, // 2: What we do (NO IMAGE)
|
||||
'uploads/2024/12/1694273920124-copy.webp', // 3: Legacy (Matching Team page)
|
||||
'uploads/2024/12/1694273920124-copy-2.webp', // 4: Experience (Matching Home page)
|
||||
null, // 5: Why choose us (NO IMAGE)
|
||||
'uploads/2024/12/DSC08036-Large.webp', // 6: Team (Matching Team page)
|
||||
null, // 7: Manifesto (NO IMAGE)
|
||||
'uploads/2024/12/DSC07433-Large-600x400.webp', // 8: Back cover
|
||||
null, // 2: Was wir tun (NO IMAGE — text-heavy)
|
||||
'uploads/2024/12/1694273920124-copy.webp', // 3: Erfahrung & Qualität
|
||||
'uploads/2024/12/DSC07433-Large-600x400.webp', // 4: Back cover
|
||||
];
|
||||
|
||||
const galleryImages: (string | Buffer | undefined)[] = [];
|
||||
@@ -447,12 +415,35 @@ async function main(): Promise<void> {
|
||||
messages = JSON.parse(fs.readFileSync(messagesPath, 'utf-8'));
|
||||
} catch { /* messages are optional */ }
|
||||
|
||||
// Load director portrait photos and crop to circles
|
||||
const directorPhotos: { michael?: Buffer; klaus?: Buffer } = {};
|
||||
const portraitPaths = {
|
||||
michael: path.join(process.cwd(), 'public/uploads/2024/12/DSC07768-Large.webp'),
|
||||
klaus: path.join(process.cwd(), 'public/uploads/2024/12/DSC07963-Large.webp'),
|
||||
};
|
||||
const AVATAR_SIZE = 120; // px, will be rendered at 32pt in PDF
|
||||
const circleMask = Buffer.from(
|
||||
`<svg width="${AVATAR_SIZE}" height="${AVATAR_SIZE}"><circle cx="${AVATAR_SIZE / 2}" cy="${AVATAR_SIZE / 2}" r="${AVATAR_SIZE / 2}" fill="white"/></svg>`
|
||||
);
|
||||
for (const [key, photoPath] of Object.entries(portraitPaths)) {
|
||||
if (fs.existsSync(photoPath)) {
|
||||
try {
|
||||
const cropped = await sharp(photoPath)
|
||||
.resize(AVATAR_SIZE, AVATAR_SIZE, { fit: 'cover', position: 'top' })
|
||||
.composite([{ input: circleMask, blend: 'dest-in' }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
directorPhotos[key as 'michael' | 'klaus'] = cropped;
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Render the React-PDF brochure
|
||||
const buffer = await renderToBuffer(
|
||||
React.createElement(PDFBrochure, {
|
||||
products, locale, companyInfo, introContent,
|
||||
marketingSections, logoBlack, logoWhite, galleryImages, messages,
|
||||
marketingSections, logoBlack, logoWhite, galleryImages, messages, directorPhotos,
|
||||
} as any) as any
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user