import * as React from 'react';
import { Document, Page, View, Text, Image, StyleSheet, Font, Link } from '@react-pdf/renderer';
// Register fonts (using system fonts for now, can be customized)
Font.register({
family: 'Helvetica',
fonts: [
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
],
});
// ─── Brand Tokens ────────────────────────────────────────
const C = {
navy: '#001a4d',
navyDeep: '#000d26',
green: '#4da612',
white: '#FFFFFF',
offWhite: '#f8f9fa',
gray200: '#e5e7eb',
gray400: '#9ca3af',
gray600: '#4b5563',
gray900: '#111827',
};
const MARGIN = 56;
const HEADER_H = 52;
const FOOTER_H = 48;
const BODY_TOP = HEADER_H + 20;
const styles = StyleSheet.create({
page: {
color: C.gray900,
lineHeight: 1.5,
backgroundColor: C.white,
paddingTop: BODY_TOP,
paddingBottom: FOOTER_H + 40,
paddingHorizontal: MARGIN,
fontFamily: 'Helvetica',
},
header: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: HEADER_H,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-end',
paddingHorizontal: MARGIN,
paddingBottom: 12,
borderBottomWidth: 0,
},
logoText: {
fontSize: 16,
fontWeight: 700,
color: C.navy,
},
docTitleLabel: {
fontSize: 7,
fontWeight: 700,
color: C.gray400,
letterSpacing: 1.2,
textTransform: 'uppercase',
},
footer: {
position: 'absolute',
bottom: 20,
left: MARGIN,
right: MARGIN,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderTopWidth: 0.5,
borderTopColor: C.gray200,
paddingTop: 8,
},
footerText: {
fontSize: 7,
color: C.gray400,
letterSpacing: 0.8,
textTransform: 'uppercase',
},
pageTitle: {
fontSize: 24,
fontWeight: 700,
color: C.navyDeep,
marginBottom: 16,
textTransform: 'uppercase',
letterSpacing: -0.5,
},
accentBar: {
width: 40,
height: 3,
backgroundColor: C.green,
marginBottom: 24,
},
// Lexical Elements
paragraph: {
fontSize: 10,
color: C.gray600,
lineHeight: 1.6,
marginBottom: 12,
},
heading1: {
fontSize: 18,
fontWeight: 700,
color: C.navyDeep,
marginTop: 16,
marginBottom: 8,
},
heading2: {
fontSize: 14,
fontWeight: 700,
color: C.navyDeep,
marginTop: 14,
marginBottom: 6,
},
heading3: {
fontSize: 12,
fontWeight: 700,
color: C.navyDeep,
marginTop: 12,
marginBottom: 4,
},
list: {
marginBottom: 12,
marginLeft: 8,
},
listItem: {
flexDirection: 'row',
marginBottom: 4,
},
listItemBullet: {
width: 12,
fontSize: 10,
color: C.green,
},
listItemContent: {
flex: 1,
fontSize: 10,
color: C.gray600,
lineHeight: 1.6,
},
link: {
color: C.green,
textDecoration: 'none',
},
textBold: {
fontWeight: 700,
fontFamily: 'Helvetica-Bold',
color: C.navyDeep,
},
textItalic: {
fontStyle: 'italic', // Not actually working without proper font set, but we will fallback
},
});
// ─── Lexical to React-PDF Renderer ────────────────────────────────
const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
if (!node) return null;
switch (node.type) {
case 'text': {
const format = node.format || 0;
const isBold = (format & 1) !== 0;
const isItalic = (format & 2) !== 0;
let elementStyle: any = {};
if (isBold) elementStyle = { ...elementStyle, ...styles.textBold };
if (isItalic) elementStyle = { ...elementStyle, ...styles.textItalic };
return (
{node.text}
);
}
case 'paragraph': {
return (
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
);
}
case 'heading': {
let hStyle = styles.heading3;
if (node.tag === 'h1') hStyle = styles.heading1;
if (node.tag === 'h2') hStyle = styles.heading2;
return (
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
);
}
case 'list': {
return (
{node.children?.map((child: any, i: number) => {
if (child.type === 'listitem') {
return (
{node.listType === 'number' ? `${i + 1}.` : '•'}
{child.children?.map((c: any, ci: number) => renderLexicalNode(c, ci))}
);
}
return renderLexicalNode(child, i);
})}
);
}
case 'link': {
const href = node.fields?.url || node.url || '#';
return (
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
);
}
case 'linebreak': {
// React-PDF doesn't handle `
`, but a newline char usually works inside ``.
return {'\n'};
}
// Ignore payload blocks recursively to avoid crashing, as pages should mainly use rich text
case 'block':
return null;
default:
if (node.children) {
return (
{node.children.map((child: any, i: number) => renderLexicalNode(child, i))}
);
}
return null;
}
};
interface PDFPageProps {
page: any;
locale?: string;
}
export const PDFPage: React.FC = ({ page, locale = 'de' }) => {
const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
return (
KLZ
{locale === 'en' ? 'Document' : 'Dokument'}
{page.title}
{page.content?.root?.children?.map((node: any, i: number) => renderLexicalNode(node, i))}
KLZ CABLES
{dateStr}
);
};