Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 55s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m32s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m50s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
402 lines
10 KiB
TypeScript
402 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import {
|
|
View as PDFView,
|
|
Text as PDFText,
|
|
StyleSheet,
|
|
Image as PDFImage,
|
|
} from "@react-pdf/renderer";
|
|
|
|
// INDUSTRIAL DESIGN SYSTEM TOKENS
|
|
export const COLORS = {
|
|
CHARCOAL: "#0f172a", // Slate 900
|
|
TEXT_MAIN: "#334155", // Slate 700
|
|
TEXT_DIM: "#64748b", // Slate 500
|
|
TEXT_LIGHT: "#94a3b8", // Slate 400
|
|
DIVIDER: "#cbd5e1", // Slate 300
|
|
GRID: "#f1f5f9", // Slate 100
|
|
BLUEPRINT: "#e2e8f0", // Slate 200
|
|
WHITE: "#ffffff",
|
|
};
|
|
|
|
export const FONT_SIZES = {
|
|
HERO: 24, // Main Page Titles
|
|
HEADING: 14, // Section Headers
|
|
BODY: 11, // Standard Content
|
|
LABEL: 10, // Bold Labels / Keys
|
|
SMALL: 9, // Descriptions / Footnotes
|
|
TINY: 8, // Metadata / Unit prices
|
|
};
|
|
|
|
export const pdfStyles = StyleSheet.create({
|
|
page: {
|
|
paddingTop: 45, // DIN 5008
|
|
paddingLeft: 70, // ~25mm
|
|
paddingRight: 57, // ~20mm
|
|
paddingBottom: 80, // Safe buffer for absolute footer
|
|
backgroundColor: COLORS.WHITE,
|
|
fontFamily: "Helvetica",
|
|
fontSize: FONT_SIZES.BODY,
|
|
color: COLORS.CHARCOAL,
|
|
},
|
|
titlePage: {
|
|
width: "100%",
|
|
height: "100%",
|
|
backgroundColor: COLORS.WHITE,
|
|
fontFamily: "Helvetica",
|
|
color: COLORS.CHARCOAL,
|
|
padding: 0,
|
|
},
|
|
header: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "flex-start",
|
|
marginBottom: 20,
|
|
minHeight: 120,
|
|
},
|
|
addressBlock: {
|
|
width: "55%",
|
|
marginTop: 45,
|
|
},
|
|
senderLine: {
|
|
fontSize: FONT_SIZES.TINY,
|
|
textDecoration: "underline",
|
|
color: COLORS.TEXT_DIM,
|
|
marginBottom: 8,
|
|
},
|
|
recipientAddress: {
|
|
fontSize: FONT_SIZES.BODY,
|
|
lineHeight: 1.4,
|
|
},
|
|
brandLogoContainer: {
|
|
width: "40%",
|
|
alignItems: "flex-end",
|
|
},
|
|
brandIconContainer: {
|
|
width: 40,
|
|
height: 40,
|
|
backgroundColor: "#0f172a",
|
|
borderRadius: 8,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
marginBottom: 12,
|
|
},
|
|
brandIconText: {
|
|
color: COLORS.WHITE,
|
|
fontSize: 20,
|
|
fontWeight: "bold",
|
|
},
|
|
titleInfo: {
|
|
marginBottom: 24,
|
|
},
|
|
mainTitle: {
|
|
fontSize: FONT_SIZES.HEADING,
|
|
fontWeight: "bold",
|
|
marginBottom: 4,
|
|
color: COLORS.CHARCOAL,
|
|
letterSpacing: 0.5,
|
|
},
|
|
subTitle: {
|
|
fontSize: FONT_SIZES.BODY,
|
|
color: COLORS.TEXT_DIM,
|
|
marginTop: 2,
|
|
lineHeight: 1.4,
|
|
},
|
|
section: {
|
|
marginBottom: 32,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: FONT_SIZES.LABEL,
|
|
fontWeight: "bold",
|
|
textTransform: "uppercase",
|
|
letterSpacing: 1,
|
|
color: COLORS.TEXT_LIGHT,
|
|
marginBottom: 8,
|
|
},
|
|
footer: {
|
|
position: "absolute",
|
|
bottom: 32,
|
|
left: 70,
|
|
right: 57,
|
|
borderTopWidth: 1,
|
|
borderTopColor: COLORS.GRID,
|
|
paddingTop: 16,
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "flex-start",
|
|
},
|
|
footerColumn: {
|
|
flex: 1,
|
|
alignItems: "flex-start",
|
|
},
|
|
footerLogo: {
|
|
height: 20,
|
|
width: "auto",
|
|
objectFit: "contain",
|
|
marginBottom: 8,
|
|
},
|
|
footerText: {
|
|
fontSize: FONT_SIZES.TINY,
|
|
color: COLORS.TEXT_LIGHT,
|
|
lineHeight: 1.4,
|
|
},
|
|
asymmetryContainer: {
|
|
flexDirection: "row",
|
|
gap: 32,
|
|
},
|
|
asymmetryLeft: {
|
|
width: "32%",
|
|
},
|
|
asymmetryRight: {
|
|
width: "63%",
|
|
},
|
|
specRow: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
paddingVertical: 6,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: COLORS.GRID,
|
|
},
|
|
specLabel: {
|
|
fontSize: FONT_SIZES.TINY,
|
|
fontWeight: "bold",
|
|
color: COLORS.TEXT_LIGHT,
|
|
textTransform: "uppercase",
|
|
letterSpacing: 0.5,
|
|
},
|
|
specValue: {
|
|
fontSize: FONT_SIZES.SMALL,
|
|
color: COLORS.CHARCOAL,
|
|
fontWeight: "bold",
|
|
},
|
|
blueprintBox: {
|
|
borderWidth: 1,
|
|
borderColor: COLORS.GRID,
|
|
padding: 16,
|
|
backgroundColor: "#fafafa",
|
|
},
|
|
footerLabel: {
|
|
fontWeight: "bold",
|
|
color: COLORS.TEXT_DIM,
|
|
},
|
|
pageNumber: {
|
|
fontSize: FONT_SIZES.TINY,
|
|
color: COLORS.DIVIDER,
|
|
fontWeight: "bold",
|
|
marginTop: 8,
|
|
textAlign: "right",
|
|
},
|
|
foldingMark: {
|
|
position: "absolute",
|
|
left: 20,
|
|
width: 10,
|
|
borderTopWidth: 0.5,
|
|
borderTopColor: COLORS.DIVIDER,
|
|
},
|
|
divider: {
|
|
width: "100%",
|
|
height: 1,
|
|
backgroundColor: COLORS.DIVIDER,
|
|
marginVertical: 12,
|
|
},
|
|
industrialListItem: {
|
|
flexDirection: "row",
|
|
alignItems: "flex-start",
|
|
marginBottom: 6,
|
|
},
|
|
industrialBulletBox: {
|
|
width: 6,
|
|
height: 6,
|
|
backgroundColor: COLORS.DIVIDER,
|
|
marginRight: 8,
|
|
marginTop: 5,
|
|
},
|
|
industrialTitle: {
|
|
fontSize: FONT_SIZES.HERO,
|
|
fontWeight: "bold",
|
|
color: COLORS.CHARCOAL,
|
|
marginBottom: 6,
|
|
letterSpacing: 0,
|
|
},
|
|
});
|
|
|
|
export const IndustrialListItem = ({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) => (
|
|
<PDFView style={pdfStyles.industrialListItem}>
|
|
<PDFView style={pdfStyles.industrialBulletBox} />
|
|
{children}
|
|
</PDFView>
|
|
);
|
|
|
|
export const Divider = ({ style = {} }: { style?: any }) => (
|
|
<PDFView style={[pdfStyles.divider, style]} />
|
|
);
|
|
|
|
export const Footer = ({
|
|
logo,
|
|
companyData,
|
|
showDetails = true,
|
|
showPageNumber = true,
|
|
}: {
|
|
logo?: string;
|
|
companyData: any;
|
|
showDetails?: boolean;
|
|
showPageNumber?: boolean;
|
|
}) => (
|
|
<PDFView style={pdfStyles.footer}>
|
|
<PDFView style={pdfStyles.footerColumn}>
|
|
{logo ? (
|
|
<PDFImage src={logo} style={pdfStyles.footerLogo} />
|
|
) : (
|
|
<PDFText style={{ fontSize: 12, fontWeight: "bold", marginBottom: 8 }}>
|
|
marc mintel
|
|
</PDFText>
|
|
)}
|
|
</PDFView>
|
|
{showDetails && (
|
|
<>
|
|
<PDFView style={pdfStyles.footerColumn}>
|
|
<PDFText style={pdfStyles.footerText}>
|
|
<PDFText style={pdfStyles.footerLabel}>{companyData.name}</PDFText>
|
|
{"\n"}
|
|
{companyData.address1}
|
|
{"\n"}
|
|
{companyData.address2}
|
|
{"\n"}UST: {companyData.ustId}
|
|
</PDFText>
|
|
</PDFView>
|
|
<PDFView style={[pdfStyles.footerColumn, { alignItems: "flex-end" }]}>
|
|
{showPageNumber && (
|
|
<PDFText
|
|
style={pdfStyles.pageNumber}
|
|
render={({ pageNumber, totalPages }) =>
|
|
`${pageNumber} / ${totalPages}`
|
|
}
|
|
fixed
|
|
/>
|
|
)}
|
|
</PDFView>
|
|
</>
|
|
)}
|
|
</PDFView>
|
|
);
|
|
|
|
export const Header = ({
|
|
sender,
|
|
recipient,
|
|
icon,
|
|
showAddress = true,
|
|
}: {
|
|
sender?: string;
|
|
recipient?: {
|
|
title: string;
|
|
subtitle?: string;
|
|
email?: string;
|
|
address?: string;
|
|
phone?: string;
|
|
taxId?: string;
|
|
};
|
|
icon?: string;
|
|
showAddress?: boolean;
|
|
}) => (
|
|
<PDFView
|
|
style={[
|
|
pdfStyles.header,
|
|
showAddress ? {} : { minHeight: 40, marginBottom: 0 },
|
|
]}
|
|
>
|
|
<PDFView style={pdfStyles.addressBlock}>
|
|
{showAddress && sender && (
|
|
<>
|
|
<PDFText style={pdfStyles.senderLine}>{sender}</PDFText>
|
|
{recipient && (
|
|
<PDFView style={pdfStyles.recipientAddress}>
|
|
<PDFText style={{ fontWeight: "bold" }}>
|
|
{recipient.title}
|
|
</PDFText>
|
|
{recipient.subtitle && <PDFText>{recipient.subtitle}</PDFText>}
|
|
{recipient.address && <PDFText>{recipient.address}</PDFText>}
|
|
{recipient.phone && <PDFText>{recipient.phone}</PDFText>}
|
|
{recipient.email && <PDFText>{recipient.email}</PDFText>}
|
|
{recipient.taxId && <PDFText>USt-ID: {recipient.taxId}</PDFText>}
|
|
</PDFView>
|
|
)}
|
|
</>
|
|
)}
|
|
</PDFView>
|
|
<PDFView style={pdfStyles.brandLogoContainer}>
|
|
<PDFView style={pdfStyles.brandIconContainer}>
|
|
{icon ? (
|
|
<PDFImage src={icon} style={{ width: 24, height: 24 }} />
|
|
) : (
|
|
<PDFText style={pdfStyles.brandIconText}>M</PDFText>
|
|
)}
|
|
</PDFView>
|
|
</PDFView>
|
|
</PDFView>
|
|
);
|
|
|
|
export const DocumentTitle = ({
|
|
title,
|
|
subLines,
|
|
isHero = false,
|
|
}: {
|
|
title: string;
|
|
subLines?: string[];
|
|
isHero?: boolean;
|
|
}) => (
|
|
<PDFView style={pdfStyles.titleInfo}>
|
|
<PDFText
|
|
style={[
|
|
pdfStyles.mainTitle,
|
|
{ fontSize: isHero ? FONT_SIZES.HERO : FONT_SIZES.HEADING },
|
|
]}
|
|
>
|
|
{title}
|
|
</PDFText>
|
|
{subLines?.map((line, i) => (
|
|
<PDFText
|
|
key={i}
|
|
style={[
|
|
pdfStyles.subTitle,
|
|
i === 1 ? { fontWeight: "bold", color: COLORS.CHARCOAL } : {},
|
|
]}
|
|
>
|
|
{line}
|
|
</PDFText>
|
|
))}
|
|
</PDFView>
|
|
);
|
|
|
|
export const TechnicalSpec = ({
|
|
label,
|
|
value,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
}) => (
|
|
<PDFView style={pdfStyles.specRow}>
|
|
<PDFText style={pdfStyles.specLabel}>{label}</PDFText>
|
|
<PDFText style={pdfStyles.specValue}>{value}</PDFText>
|
|
</PDFView>
|
|
);
|
|
|
|
export const AsymmetryView = ({
|
|
left,
|
|
right,
|
|
style = {},
|
|
}: {
|
|
left: React.ReactNode;
|
|
right: React.ReactNode;
|
|
style?: any;
|
|
}) => (
|
|
<PDFView style={[pdfStyles.asymmetryContainer, style]}>
|
|
<PDFView style={pdfStyles.asymmetryLeft}>{left}</PDFView>
|
|
<PDFView style={pdfStyles.asymmetryRight}>{right}</PDFView>
|
|
</PDFView>
|
|
);
|