import React from 'react';
import * as fs from 'fs';
import {
Document,
Page,
Text,
View,
StyleSheet,
Font,
Image,
Svg,
Path,
G,
Rect,
ClipPath,
Circle,
} from '@react-pdf/renderer';
import * as path from 'path';
import { TruckBlueprint } from './TruckBlueprint';
// Register embedded fonts so Ghostscript -dNoOutputFonts can convert them to
// outlines (curves). Built-in PDF fonts like Helvetica are NEVER embedded and
// therefore cannot be outlined — always use a physical font file here.
Font.register({
family: 'Inter',
fonts: [
{
src: path.join(process.cwd(), 'public', 'fonts', 'Inter-Regular.woff'),
fontWeight: 'normal',
},
{
src: path.join(process.cwd(), 'public', 'fonts', 'Inter-Bold.woff'),
fontWeight: 'bold',
},
],
});
export interface PersonData {
name: string;
role: string;
phone: string;
email: string;
}
export interface PDFBusinessCardProps {
person: PersonData;
qrCodeDataUrl: string;
variant?: 'blue' | 'white' | 'black';
isViaprinto?: boolean;
withTruck?: boolean;
withCropMarks?: boolean;
}
// Die Buchstaben K, L, Z im SVG (inklusive dem kleinen Stück vom Z oben links)
export const KLZ_PATHS = [25, 26, 27, 28, 29];
const COLORS = {
primary: '#000a66', // Brand Dark Blue (from website)
blue: '#000a66',
green: '#82ed20', // Brand Accent Green
darkGreen: '#4A9E00', // Darker green for white background visibility
white: '#ffffff',
grayText: '#6c757d',
black: '#0a0a0a',
neutralDark: '#263336',
lightGray: '#f8f9fa',
};
const CropMarks = () => (
{/* Top Left */}
{/* Top Right */}
{/* Bottom Left */}
{/* Bottom Right */}
);
const styles = StyleSheet.create({
pageBleed: {
width: '91mm',
height: '61mm',
backgroundColor: COLORS.primary,
position: 'relative',
fontFamily: 'Inter',
overflow: 'hidden',
},
pageBleedBack: {
width: '91mm',
height: '61mm',
backgroundColor: COLORS.white,
position: 'relative',
fontFamily: 'Inter',
overflow: 'hidden',
},
// -- BLUE PAGE (Rückseite) --
truckWrapper: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: '20mm', // Truck stops above the footer area to avoid text conflict
zIndex: 1,
},
frontBackgroundImage: {
width: '100%',
height: '100%',
opacity: 0.4, // Less transparent, more contrast
},
frontContainer: {
flex: 1,
paddingTop: '3mm', // Compensate for top bleed for perfect optical centering
paddingBottom: '20mm', // Leave space for the footer area
justifyContent: 'center',
alignItems: 'center',
},
frontLogo: {
width: 145, // Reduced for a more elegant, premium look with more negative space
zIndex: 100,
},
frontTagline: {
color: COLORS.white,
fontSize: 5.5,
marginTop: 8,
letterSpacing: 2,
fontWeight: 'bold',
textTransform: 'uppercase',
zIndex: 100,
},
frontFooterArea: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '22mm', // Increased height to accommodate more bottom padding
paddingBottom: '10mm', // Increased bottom padding to move icons further from edge
zIndex: 50,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: 6, // Harmonious spacing to icons
},
frontIconsGroup: {
flexDirection: 'row',
gap: 14, // Wider gaps between icon groups for visual clarity at small size
},
frontBrandIconWrapper: {
alignItems: 'center',
gap: 1.5,
},
frontBrandIcon: {
width: 12, // Smaller for compact bar
height: 12,
opacity: 0.85,
},
frontBrandText: {
fontSize: 4, // Tiny but readable at 300dpi print
color: COLORS.white,
textTransform: 'uppercase',
letterSpacing: 0.6,
// Full opacity — white on blue needs to pop cleanly
},
frontTaglineBar: {
color: COLORS.green,
fontSize: 7, // Larger — as requested
letterSpacing: 0.5, // Harmonious tracking for mixed case
fontWeight: 'bold',
},
// -- WHITE PAGE (Vorderseite) --
backContainer: {
flex: 1,
paddingTop: '6mm', // Pushed down as requested
paddingLeft: '6mm',
paddingRight: '6mm',
flexDirection: 'column',
},
backHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: '5mm', // Generous breathing room
width: '100%',
},
backLogo: {
width: 60,
},
accentLineHorizontal: {
flex: 1,
height: 1.5,
backgroundColor: COLORS.primary, // Brand blue line
marginLeft: 12,
marginRight: 12,
},
accentDot: {
width: 4,
height: 4,
borderRadius: 2,
backgroundColor: COLORS.green,
marginRight: 20, // Keep away from the edge!
},
accentBoxEdge: {
width: 3,
height: 25, // Sleek vertical bar on the edge
backgroundColor: COLORS.green,
position: 'absolute',
right: '-8mm', // Bleed edge
top: -5,
},
backBody: {
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
width: '100%',
},
leftColumn: {
width: '100%', // Take full width since right column is removed
flexDirection: 'column',
},
nameSection: {
marginBottom: '2.5mm', // Reduced to fix the 'Lücke' under the role
},
nameText: {
fontSize: 13,
fontWeight: 'bold',
color: COLORS.neutralDark,
marginBottom: 1.5,
letterSpacing: -0.15,
},
roleText: {
fontSize: 6.5,
fontWeight: 'bold',
color: COLORS.darkGreen,
letterSpacing: 1.2,
textTransform: 'uppercase',
},
contactSection: {
flexDirection: 'column',
gap: 0,
},
contactRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 5, // Even rhythm between contact lines
},
iconWrapper: {
width: 12,
justifyContent: 'center',
alignItems: 'flex-start',
},
contactText: {
fontSize: 7.5,
color: COLORS.neutralDark,
fontWeight: 'normal',
paddingTop: 0.5,
letterSpacing: 0.1,
},
rightColumn: {
flexDirection: 'column',
alignItems: 'flex-end',
justifyContent: 'flex-start',
},
// Ultra Minimalist QR Area
qrContainer: {
flexDirection: 'column',
alignItems: 'center',
gap: 3,
},
qrCodeWrapper: {
width: '10mm',
height: '10mm',
border: `0.75pt solid ${COLORS.darkGreen}`,
padding: 2,
backgroundColor: '#ffffff',
},
qrCode: {
width: '100%',
height: '100%',
},
addressFooter: {
width: '100%',
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
position: 'absolute',
bottom: '10mm', // Pushed up further from the bottom edge
left: '6mm', // Match backContainer paddingLeft for flush alignment
},
addressFooterText: {
fontSize: 6.5,
color: COLORS.grayText,
letterSpacing: 0.4,
fontWeight: 'normal',
},
});
// SVG Icons
const PhoneIcon = () => (
);
const EmailIcon = () => (
);
const GlobeIcon = () => (
);
const LocationIcon = () => (
);
const ScanIcon = () => (
);
const parseSvgPaths = (filePath: string) => {
try {
const raw = fs.readFileSync(filePath, 'utf8');
const paths = [];
const pathRegex = /]+)>/g;
let match;
while ((match = pathRegex.exec(raw)) !== null) {
const attrs = match[1];
const dMatch = attrs.match(/d="([^"]+)"/);
if (!dMatch) continue;
let fill = 'white';
const fillMatch = attrs.match(/fill(?:[:=])"?([^;"\s>]+)/);
if (fillMatch) {
fill = fillMatch[1].replace(/"$/, '');
if (fill === 'none') continue;
}
paths.push({ d: dMatch[1], fill });
}
return paths;
} catch (e) {
console.error('Could not parse SVG:', filePath);
return [];
}
};
const VectorLogo = ({
paths,
style,
overrideColor,
overrideWhiteColor,
pathOverrides,
solidPaths,
solidBackgroundColor = '#000a66',
}: {
paths: { d: string; fill: string }[];
style: any;
overrideColor?: string;
overrideWhiteColor?: string;
pathOverrides?: Record;
solidPaths?: number[];
solidBackgroundColor?: string;
}) => (
);
export const IconSolar = ({ style, color = 'white' }: { style: any; color?: string }) => (
);
export const AbstractCable = ({
size = 10,
color = COLORS.green,
}: {
size?: number;
color?: string;
}) => (
);
export const IconLowVoltage = ({ style, color = 'white' }: { style: any; color?: string }) => (
);
export const IconMediumVoltage = ({ style, color = 'white' }: { style: any; color?: string }) => (
);
export const IconHighVoltage = ({ style, color = 'white' }: { style: any; color?: string }) => (
);
export const PDFBusinessCard: React.FC = ({
person,
qrCodeDataUrl,
variant = 'blue',
isViaprinto = false,
withTruck = true,
withCropMarks = false,
}) => {
const isWhiteVariant = variant === 'white' || variant === 'black';
const logoWhitePath = path.join(process.cwd(), 'public', 'logo-green-text.svg');
const logoBluePath = path.join(process.cwd(), 'public', 'logo-green-text-blue.svg');
const logoBlueWithTaglinePath = path.join(
process.cwd(),
'public',
'logo-green-text-blue-with-tagline.svg',
);
const logoBlackWithTaglinePath = path.join(
process.cwd(),
'public',
'logo-green-text-black-with-tagline.svg',
);
const logoWhitePaths = parseSvgPaths(logoWhitePath);
const logoBluePaths = parseSvgPaths(logoBluePath);
const logoBlueWithTaglinePaths = parseSvgPaths(logoBlueWithTaglinePath);
const logoBlackWithTaglinePaths = parseSvgPaths(logoBlackWithTaglinePath);
const getFrontLogoPaths = () => {
if (variant === 'black') return logoBlackWithTaglinePaths;
if (variant === 'white') return logoBlueWithTaglinePaths;
return logoWhitePaths;
};
const getBackLogoPaths = () => {
// We use the original black logo to preserve the exact colors of the Truck and KLZ.
// The tagline will be hidden via pathOverrides in the JSX.
return logoBlackWithTaglinePaths;
};
const bgImagePath = isWhiteVariant
? path.join(process.cwd(), 'public', 'media', 'cable_drums_truck.png')
: path.join(process.cwd(), 'public', 'cable_drums_bg.png');
const renderPageWrapper = (children: React.ReactNode, bleedStyle: any) => {
if (withCropMarks) {
return (
{children}
);
}
return (
{children}
);
};
return (
{/* VORDERSEITE (Blue page with logo, truck and tagline) */}
{renderPageWrapper(
<>
Von 0,6/1,0 kV bis 64/110 kV
Niederspannung
Mittelspannung
Hochspannung
Solar
{/* Base Logo: text is white, truck is green. Fill KLZ background to hide truck, keep outline white */}
{/* Letterpress Mask Layer: We use #ff00fe as a marker color. Only the "KLZ" letters! */}
{isViaprinto && (
KLZ_PATHS.includes(i))}
style={[styles.frontLogo, { position: 'absolute' }]}
overrideColor="#ff00fe"
solidPaths={KLZ_PATHS.map((_, i) => i)}
/>
)}
>,
[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }],
)}
{renderPageWrapper(
({ ...acc, [idx]: 'none' }), {}),
// Paths 0-24 are the "Cables for a greener tomorrow" tagline in the 40-path logo.
...Array.from({ length: 25 }).reduce((acc, _, i) => ({ ...acc, [i]: 'none' }), {}),
}}
/>
{person.name}
{person.role}
{person.phone}
{person.email.toLowerCase()}
klz-cables.com
KLZ Vertriebs GmbH · Raiffeisenstraße 22 · 73630 Remshalden
,
styles.pageBleedBack,
)}
);
};