diff --git a/lib/pdf-business-card.tsx b/lib/pdf-business-card.tsx index 8b837c55..67993760 100644 --- a/lib/pdf-business-card.tsx +++ b/lib/pdf-business-card.tsx @@ -1,154 +1,201 @@ -import * as React from 'react'; -import { Document, Page, View, Text, Image, StyleSheet, Font } from '@react-pdf/renderer'; -import * as path from 'path'; - -// Wir nutzen Standard-Helvetica, da das Projekt Helvetica für sauberen Tech-Look vorsieht. -const styles = StyleSheet.create({ - // Size incl. 3mm bleed on all sides - // 85x55 -> 91x61 -> 257.95 x 172.91 pt - pageFront: { - backgroundColor: '#001a4d', // KLZ Navy - width: 257.95, - height: 172.91, - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - position: 'relative', - fontFamily: 'Helvetica', - }, - pageBack: { - backgroundColor: '#FFFFFF', - width: 257.95, - height: 172.91, - flexDirection: 'column', - justifyContent: 'center', - padding: 24, - position: 'relative', - fontFamily: 'Helvetica', - }, - logo: { - width: 100, // Logo breite - }, - accentLine: { - position: 'absolute', - bottom: 0, - left: 0, - width: '100%', - height: 8, - backgroundColor: '#82ed20', // KLZ Green - }, - accentLineTop: { - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: 8, - backgroundColor: '#82ed20', // KLZ Green - }, - personInfo: { - marginBottom: 16, - }, - name: { - fontSize: 16, - fontWeight: 'bold', - color: '#000d26', - letterSpacing: -0.5, - marginBottom: 2, - textTransform: 'uppercase', - }, - position: { - fontSize: 8, - color: '#82ed20', // Highlight im Text - textTransform: 'uppercase', - letterSpacing: 1, - fontWeight: 'bold', - }, - contactGrid: { - marginTop: 10, - display: 'flex', - flexDirection: 'column', - gap: 4, - }, - contactRow: { - flexDirection: 'row', - alignItems: 'center', - }, - contactLabel: { - width: 40, - fontSize: 7, - color: '#4b5563', - fontWeight: 'bold', - textTransform: 'uppercase', - }, - contactValue: { - fontSize: 8, - color: '#111827', - }, - logoTextWhite: { - fontSize: 32, - fontWeight: 700, - color: '#FFFFFF', - letterSpacing: 4, - marginBottom: 4, - }, - logoTextBlue: { - position: 'absolute', - bottom: 24, - right: 24, - fontSize: 24, - fontWeight: 700, - color: '#001a4d', - letterSpacing: 2, - opacity: 0.3, - }, -}); +import React from 'react'; +import { Document, Page, Text, View, StyleSheet, Font, Image } from '@react-pdf/renderer'; export interface PersonData { name: string; - position: string; + role: string; phone: string; email: string; - website: string; } interface PDFBusinessCardProps { person: PersonData; + logoWhitePath: string; + logoBluePath: string; + logoFullPath: string; + truckImagePath: string; + qrCodeDataUrl: string; } -export const PDFBusinessCard: React.FC = ({ person }) => { +const styles = StyleSheet.create({ + page: { + width: '85mm', + height: '55mm', + backgroundColor: '#ffffff', + position: 'relative', + fontFamily: 'Helvetica', + }, + pageBleed: { + // 85x55mm + 3mm bleed on each side = 91x61mm + width: '91mm', + height: '61mm', + backgroundColor: '#ffffff', + position: 'relative', + fontFamily: 'Helvetica', + overflow: 'hidden', + }, + // FRONT PAGE + frontContainer: { + flex: 1, + position: 'relative', + padding: '8mm', + }, + frontLogo: { + position: 'absolute', + top: '5mm', + right: '5mm', + width: '25mm', + }, + truckImage: { + position: 'absolute', + bottom: '-2mm', + left: '-5mm', + width: '100mm', // cover bleed width + height: '50mm', + objectFit: 'contain', + }, + // BACK PAGE + backContainer: { + flex: 1, + position: 'relative', + padding: '6mm 6mm', + }, + backLogo: { + width: '20mm', + marginBottom: '8mm', + }, + backMiddle: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: '8mm', + }, + nameSection: { + flexDirection: 'column', + width: '35%', + }, + nameText: { + fontSize: 16, + fontWeight: 700, + color: '#000000', + marginBottom: 2, + }, + roleText: { + fontSize: 8, + fontWeight: 400, + color: '#333333', + }, + contactSection: { + flexDirection: 'column', + width: '45%', + paddingLeft: '2mm', + }, + contactRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 3, + }, + iconCircle: { + width: 12, + height: 12, + borderRadius: 6, + backgroundColor: '#82ed20', + justifyContent: 'center', + alignItems: 'center', + marginRight: 4, + }, + contactText: { + fontSize: 6.5, + fontWeight: 500, + color: '#000000', + }, + qrSection: { + width: '20%', + alignItems: 'flex-end', + }, + qrCode: { + width: '18mm', + height: '18mm', + }, + footer: { + position: 'absolute', + bottom: '5mm', + left: '6mm', + right: '6mm', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + footerText: { + fontSize: 6.5, + fontWeight: 500, + color: '#000000', + }, + pipe: { + fontSize: 6.5, + fontWeight: 500, + color: '#82ed20', + marginHorizontal: 3, + }, +}); + +export const PDFBusinessCard: React.FC = ({ + person, + logoFullPath, + truckImagePath, + qrCodeDataUrl, +}) => { return ( - - {/* Front: Logo and Navy Background */} - - KLZ - + + {/* FRONT - 91x61mm with Bleed */} + + + - {/* Back: Contact Details */} - - + {/* BACK - 91x61mm with Bleed */} + + + - - {person.name} - {person.position} - + + + {person.name} + {person.role} + - - - Phone - {person.phone} + + + + {person.phone} + + + + {person.email} + + + + klz-cables.com + + + + + + - - Email - {person.email} - - - Web - {person.website} + + + KLZ Vertriebs GmbH + | + Raiffeisenstraße 22 + | + 73630 Remshalden - - KLZ ); diff --git a/package.json b/package.json index 380b6985..4b5ff2bb 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "pdf-lib": "^1.17.1", "pino": "^10.3.0", "pino-pretty": "^13.1.3", + "qrcode": "^1.5.4", "react": "^19.2.4", "react-dom": "^19.2.4", "react-email": "^5.2.5", @@ -56,6 +57,7 @@ "@types/leaflet": "^1.9.21", "@types/node": "^22.19.3", "@types/nodemailer": "^7.0.5", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/sharp": "^0.31.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ce0e816..944d16ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: pino-pretty: specifier: ^13.1.3 version: 13.1.3 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 react: specifier: ^19.2.4 version: 19.2.4 @@ -163,6 +166,9 @@ importers: '@types/nodemailer': specifier: ^7.0.5 version: 7.0.11 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 '@types/react': specifier: ^19.2.7 version: 19.2.14 @@ -2801,6 +2807,9 @@ packages: '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -3990,6 +3999,9 @@ packages: dfa@1.2.0: resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -6167,6 +6179,10 @@ packages: png-js@1.0.0: resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + po-parser@2.1.1: resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==} @@ -6286,6 +6302,11 @@ packages: engines: {node: '>=18'} hasBin: true + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + qs@6.14.2: resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} engines: {node: '>=0.6'} @@ -6964,8 +6985,8 @@ packages: third-party-web@0.26.7: resolution: {integrity: sha512-buUzX4sXC4efFX6xg2bw6/eZsCUh8qQwSavC4D9HpONMFlRbcHhD8Je5qwYdCpViR6q0qla2wPP+t91a2vgolg==} - third-party-web@0.29.0: - resolution: {integrity: sha512-nBDSJw5B7Sl1YfsATG2XkW5qgUPODbJhXw++BKygi9w6O/NKS98/uY/nR/DxDq2axEjL6halHW1v+jhm/j1DBQ==} + third-party-web@0.29.2: + resolution: {integrity: sha512-fegtha91tq2DHphyoiBXVHjVi2YG9zFaRnboT9C28tO1en9Y3wJsfspuy40F+u5wl3hHVbw7cnd1b67kEGHb8g==} thread-stream@4.0.0: resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} @@ -9121,7 +9142,7 @@ snapshots: '@paulirish/trace_engine@0.0.53': dependencies: legacy-javascript: 0.0.1 - third-party-web: 0.29.0 + third-party-web: 0.29.2 '@pdf-lib/standard-fonts@1.0.0': dependencies: @@ -10152,6 +10173,10 @@ snapshots: pg-protocol: 1.13.0 pg-types: 2.2.0 + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 22.19.15 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -11442,6 +11467,8 @@ snapshots: dfa@1.2.0: {} + dijkstrajs@1.0.3: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -14186,6 +14213,8 @@ snapshots: png-js@1.0.0: {} + pngjs@5.0.0: {} + po-parser@2.1.1: {} possible-typed-array-names@1.1.0: {} @@ -14317,6 +14346,12 @@ snapshots: - typescript - utf-8-validate + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + qs@6.14.2: dependencies: side-channel: 1.1.0 @@ -15196,7 +15231,7 @@ snapshots: third-party-web@0.26.7: {} - third-party-web@0.29.0: {} + third-party-web@0.29.2: {} thread-stream@4.0.0: dependencies: diff --git a/public/logo-blue.png b/public/logo-blue.png new file mode 100644 index 00000000..68bc16d9 Binary files /dev/null and b/public/logo-blue.png differ diff --git a/public/logo-white.png b/public/logo-white.png new file mode 100644 index 00000000..5401314d Binary files /dev/null and b/public/logo-white.png differ diff --git a/public/truck.png b/public/truck.png new file mode 100644 index 00000000..6be6ed8a Binary files /dev/null and b/public/truck.png differ diff --git a/scripts/generate-business-cards.ts b/scripts/generate-business-cards.ts index 927d47c6..6e0d533e 100644 --- a/scripts/generate-business-cards.ts +++ b/scripts/generate-business-cards.ts @@ -9,45 +9,34 @@ import * as React from 'react'; import { renderToBuffer } from '@react-pdf/renderer'; import { execSync } from 'child_process'; import { PDFBusinessCard, PersonData } from '../lib/pdf-business-card'; +import QRCode from 'qrcode'; const CONFIG = { outputDir: path.join(process.cwd(), 'public/downloads/business-cards'), + logoWhite: path.join(process.cwd(), 'public/logo-white.png'), + logoBlue: path.join(process.cwd(), 'public/logo-blue.png'), + logoFull: path.join(process.cwd(), 'public/logo-full.png'), + truckImage: path.join(process.cwd(), 'public/truck.png'), }; const PEOPLE: PersonData[] = [ { name: 'Michael Bodemer', - position: 'Owner & Managing Director', - phone: '+49 151 462 467 98', + role: 'Managing Director', + phone: '+49 171 785 2420', email: 'michael.bodemer@klz-cables.com', - website: 'www.klz-cables.com', }, { name: 'Klaus Mintel', - position: 'Managing Director', + role: 'Managing Director', phone: '+49 151 1775 2873', email: 'klaus.mintel@klz-cables.com', - website: 'www.klz-cables.com', }, ]; -async function ensureOutputDir() { - if (!fs.existsSync(CONFIG.outputDir)) { - fs.mkdirSync(CONFIG.outputDir, { recursive: true }); - } -} - -/** - * Konvertiert ein RGB PDF zu CMYK via Ghostscript. - */ function convertToCMYK(inputPath: string, outputPath: string) { console.log(`- Converting to CMYK via Ghostscript...`); try { - // Basic CMYK conversion via Ghostscript. - // -dSAFER -dBATCH -dNOPAUSE - // -sDEVICE=pdfwrite - // -sColorConversionStrategy=CMYK - // -dProcessColorModel=/DeviceCMYK const gsCommand = `gs -dSAFER -dBATCH -dNOPAUSE -dNOCACHE -sDEVICE=pdfwrite -sColorConversionStrategy=CMYK -dProcessColorModel=/DeviceCMYK -dAutoFilterColorImages=false -dColorImageFilter=/FlateEncode -sOutputFile="${outputPath}" "${inputPath}"`; execSync(gsCommand, { stdio: 'pipe' }); @@ -59,11 +48,33 @@ function convertToCMYK(inputPath: string, outputPath: string) { } async function generateBusinessCards() { - await ensureOutputDir(); + if (!fs.existsSync(CONFIG.outputDir)) { + fs.mkdirSync(CONFIG.outputDir, { recursive: true }); + } for (const person of PEOPLE) { console.log(`\nGenerating Business Card for: ${person.name}`); + // Generate VCard QR Code + const lastName = person.name.split(' ').slice(1).join(' '); + const firstName = person.name.split(' ')[0]; + const vcard = `BEGIN:VCARD +VERSION:3.0 +N:${lastName};${firstName};;; +FN:${person.name} +ORG:KLZ Vertriebs GmbH +TITLE:${person.role} +TEL;TYPE=WORK,VOICE:${person.phone} +EMAIL;TYPE=PREF,INTERNET:${person.email} +URL:https://klz-cables.com +END:VCARD`; + + const qrCodeDataUrl = await QRCode.toDataURL(vcard, { + width: 400, + margin: 0, + color: { dark: '#000000', light: '#ffffff' }, + }); + const safeName = person.name.replace(/\s+/g, '_'); const rgbFileName = `KLZ_BusinessCard_${safeName}_RGB.pdf`; const cmykFileName = `KLZ_BusinessCard_${safeName}_CMYK_Print.pdf`; @@ -75,6 +86,11 @@ async function generateBusinessCards() { const buffer = await renderToBuffer( React.createElement(PDFBusinessCard, { person, + logoWhitePath: CONFIG.logoWhite, + logoBluePath: CONFIG.logoBlue, + logoFullPath: CONFIG.logoFull, + truckImagePath: CONFIG.truckImage, + qrCodeDataUrl, }), ); @@ -93,7 +109,7 @@ async function main() { await generateBusinessCards(); console.log(`\n✅ Finished in ${((Date.now() - start) / 1000).toFixed(2)}s`); } catch (error) { - console.error('\\n❌ Fatal error:', error); + console.error('\n❌ Fatal error:', error); process.exit(1); } }