feat: product catalog

This commit is contained in:
2026-03-02 16:20:54 +01:00
parent e9ceae3989
commit 501f9659a1
4 changed files with 112 additions and 115 deletions

View File

@@ -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
);