Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 2m20s
Build & Deploy / 🏗️ Build (push) Successful in 3m56s
Build & Deploy / 🚀 Deploy (push) Successful in 22s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 4m30s
Build & Deploy / 🔔 Notify (push) Successful in 2s
619 lines
19 KiB
TypeScript
619 lines
19 KiB
TypeScript
import * as React from 'react';
|
|
import { Document, Page, View, Text, Image, StyleSheet, Font } from '@react-pdf/renderer';
|
|
import type { DatasheetVoltageTable, KeyValueItem } from '../scripts/pdf/model/types';
|
|
|
|
// Standard built-in fonts are used.
|
|
Font.registerHyphenationCallback((word) => [word]);
|
|
|
|
const C = {
|
|
navy: '#001a4d',
|
|
navyDeep: '#000d26',
|
|
green: '#4da612',
|
|
greenLight: '#e8f5d8',
|
|
white: '#FFFFFF',
|
|
offWhite: '#f8f9fa',
|
|
gray100: '#f3f4f6',
|
|
gray200: '#e5e7eb',
|
|
gray300: '#d1d5db',
|
|
gray400: '#9ca3af',
|
|
gray600: '#4b5563',
|
|
gray900: '#111827',
|
|
};
|
|
|
|
const MARGIN = 56;
|
|
|
|
const styles = StyleSheet.create({
|
|
page: {
|
|
paddingHorizontal: MARGIN,
|
|
paddingBottom: 80,
|
|
paddingTop: 40,
|
|
fontFamily: 'Helvetica',
|
|
backgroundColor: C.white,
|
|
color: C.gray900,
|
|
},
|
|
hero: { paddingBottom: 20, marginBottom: 10 },
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: 16,
|
|
},
|
|
logoText: {
|
|
fontSize: 22,
|
|
fontWeight: 700,
|
|
color: C.navyDeep,
|
|
letterSpacing: 2,
|
|
textTransform: 'uppercase',
|
|
},
|
|
docTitle: {
|
|
fontSize: 8,
|
|
fontWeight: 700,
|
|
color: C.green,
|
|
letterSpacing: 2,
|
|
textTransform: 'uppercase',
|
|
},
|
|
productRow: { flexDirection: 'row', alignItems: 'center', gap: 20 },
|
|
productInfoCol: { flex: 1, justifyContent: 'center' },
|
|
productImageCol: {
|
|
flex: 1,
|
|
height: 120,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
borderRadius: 4,
|
|
borderWidth: 1,
|
|
borderColor: C.gray200,
|
|
backgroundColor: C.white,
|
|
overflow: 'hidden',
|
|
},
|
|
productName: {
|
|
fontSize: 24,
|
|
fontWeight: 700,
|
|
color: C.navyDeep,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: -0.5,
|
|
},
|
|
productMeta: {
|
|
fontSize: 10,
|
|
color: C.gray600,
|
|
fontWeight: 700,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 1,
|
|
marginBottom: 2,
|
|
},
|
|
heroImage: { width: '100%', height: '100%', objectFit: 'contain' },
|
|
noImage: { fontSize: 8, color: C.gray400, textAlign: 'center' },
|
|
|
|
content: {},
|
|
section: { marginBottom: 20 },
|
|
sectionTitle: {
|
|
fontSize: 8,
|
|
fontWeight: 700,
|
|
color: C.green,
|
|
marginBottom: 6,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 1.5,
|
|
},
|
|
sectionAccent: {
|
|
width: 30,
|
|
height: 2,
|
|
backgroundColor: C.green,
|
|
marginBottom: 8,
|
|
borderRadius: 1,
|
|
},
|
|
description: { fontSize: 10, lineHeight: 1.7, color: C.gray600 },
|
|
|
|
categories: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 },
|
|
categoryTag: {
|
|
backgroundColor: C.offWhite,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
borderWidth: 0.5,
|
|
borderColor: C.gray200,
|
|
borderRadius: 3,
|
|
},
|
|
categoryText: {
|
|
fontSize: 7,
|
|
color: C.gray600,
|
|
fontWeight: 700,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
},
|
|
|
|
footer: {
|
|
position: 'absolute',
|
|
bottom: 28,
|
|
left: MARGIN,
|
|
right: MARGIN,
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingTop: 12,
|
|
borderTopWidth: 2,
|
|
borderTopColor: C.green,
|
|
},
|
|
footerText: {
|
|
fontSize: 7,
|
|
color: C.gray400,
|
|
fontWeight: 400,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.8,
|
|
},
|
|
footerBrand: {
|
|
fontSize: 9,
|
|
fontWeight: 700,
|
|
color: C.navyDeep,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 1.5,
|
|
},
|
|
|
|
kvGrid: { width: '100%', borderWidth: 1, borderColor: C.gray200 },
|
|
kvRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: C.gray200 },
|
|
kvRowAlt: { backgroundColor: C.offWhite },
|
|
kvRowLast: { borderBottomWidth: 0 },
|
|
kvCell: { paddingVertical: 6, paddingHorizontal: 8 },
|
|
kvMidDivider: { borderRightWidth: 1, borderRightColor: C.gray200 },
|
|
kvLabelText: { fontSize: 8.5, fontWeight: 700, color: C.gray600 },
|
|
kvValueText: { fontSize: 9.5, color: C.gray900 },
|
|
|
|
tableWrap: { width: '100%', borderWidth: 1, borderColor: C.gray200, marginBottom: 14 },
|
|
tableHeader: {
|
|
width: '100%',
|
|
flexDirection: 'row',
|
|
backgroundColor: C.white,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: C.gray200,
|
|
},
|
|
tableHeaderCell: {
|
|
paddingVertical: 5,
|
|
paddingHorizontal: 4,
|
|
fontSize: 6.6,
|
|
fontWeight: 700,
|
|
color: C.navyDeep,
|
|
},
|
|
tableHeaderCellCfg: { paddingHorizontal: 6 },
|
|
tableHeaderCellDivider: { borderRightWidth: 1, borderRightColor: C.gray200 },
|
|
tableRow: {
|
|
width: '100%',
|
|
flexDirection: 'row',
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: C.gray200,
|
|
},
|
|
tableRowAlt: { backgroundColor: C.offWhite },
|
|
tableCell: { paddingVertical: 4, paddingHorizontal: 4, fontSize: 6.6, color: C.gray900 },
|
|
tableCellCfg: { paddingHorizontal: 6 },
|
|
tableCellDivider: { borderRightWidth: 1, borderRightColor: C.gray200 },
|
|
});
|
|
|
|
interface ProductData {
|
|
id: number;
|
|
name: string;
|
|
sku: string;
|
|
categoriesLine?: string;
|
|
descriptionText?: string;
|
|
heroSrc?: string | null;
|
|
productUrl?: string;
|
|
shortDescriptionHtml?: string;
|
|
descriptionHtml?: string;
|
|
applicationHtml?: string;
|
|
images?: string[];
|
|
featuredImage?: string | null;
|
|
logoDataUrl?: string | null;
|
|
categories?: Array<{ name: string }>;
|
|
attributes?: Array<{ name: string; options: string[] }>;
|
|
}
|
|
|
|
export interface PDFDatasheetProps {
|
|
product: ProductData;
|
|
locale: 'en' | 'de';
|
|
logoDataUrl?: string | null;
|
|
technicalItems?: KeyValueItem[];
|
|
voltageTables?: DatasheetVoltageTable[];
|
|
legendItems?: KeyValueItem[];
|
|
}
|
|
|
|
const stripHtml = (html: string): string => html.replace(/<[^>]*>/g, '');
|
|
|
|
const getLabels = (locale: 'en' | 'de') =>
|
|
({
|
|
en: {
|
|
productDatasheet: 'Technical Datasheet',
|
|
description: 'APPLICATION',
|
|
specifications: 'TECHNICAL DATA',
|
|
categories: 'CATEGORIES',
|
|
sku: 'SKU',
|
|
noImage: 'No image available',
|
|
crossSection: 'Configurations',
|
|
slug_cs: 'Cores & CS',
|
|
abbreviations: 'ABBREVIATIONS',
|
|
},
|
|
de: {
|
|
productDatasheet: 'Technisches Datenblatt',
|
|
description: 'ANWENDUNG',
|
|
specifications: 'TECHNISCHE DATEN',
|
|
categories: 'KATEGORIEN',
|
|
sku: 'ARTIKELNUMMER',
|
|
noImage: 'Kein Bild verfügbar',
|
|
crossSection: 'Konfigurationen',
|
|
slug_cs: 'Adern & QS',
|
|
abbreviations: 'ABKÜRZUNGEN',
|
|
},
|
|
})[locale];
|
|
|
|
function clamp(n: number, min: number, max: number) {
|
|
return Math.max(min, Math.min(max, n));
|
|
}
|
|
function normTextForMeasure(v: unknown) {
|
|
return String(v ?? '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
function textLen(v: unknown) {
|
|
return normTextForMeasure(v).length;
|
|
}
|
|
|
|
function distributeWithMinMax(
|
|
weights: number[],
|
|
total: number,
|
|
minEach: number,
|
|
maxEach: number,
|
|
): number[] {
|
|
const n = weights.length;
|
|
if (!n) return [];
|
|
const mins = Array.from({ length: n }, () => minEach);
|
|
const maxs = Array.from({ length: n }, () => maxEach);
|
|
const minSum = mins.reduce((a, b) => a + b, 0);
|
|
if (minSum > total) return mins.map((m) => m * (total / minSum));
|
|
|
|
const result = mins.slice();
|
|
let remaining = total - minSum;
|
|
let remainingIdx = Array.from({ length: n }, (_, i) => i);
|
|
|
|
while (remaining > 1e-9 && remainingIdx.length) {
|
|
const wSum = remainingIdx.reduce((acc, i) => acc + Math.max(0, weights[i] || 0), 0);
|
|
if (wSum <= 1e-9) {
|
|
const even = remaining / remainingIdx.length;
|
|
for (const i of remainingIdx) result[i] += even;
|
|
remaining = 0;
|
|
break;
|
|
}
|
|
const nextIdx: number[] = [];
|
|
for (const i of remainingIdx) {
|
|
const w = Math.max(0, weights[i] || 0);
|
|
const add = (w / wSum) * remaining;
|
|
const capped = Math.min(result[i] + add, maxs[i]);
|
|
const used = capped - result[i];
|
|
result[i] = capped;
|
|
remaining -= used;
|
|
if (result[i] + 1e-9 < maxs[i]) nextIdx.push(i);
|
|
}
|
|
remainingIdx = nextIdx;
|
|
}
|
|
const sum = result.reduce((a, b) => a + b, 0);
|
|
const drift = total - sum;
|
|
if (Math.abs(drift) > 1e-9) result[result.length - 1] += drift;
|
|
return result;
|
|
}
|
|
|
|
function KeyValueGrid({ items }: { items: KeyValueItem[] }) {
|
|
const filtered = (items || []).filter((i) => i.label && i.value);
|
|
if (!filtered.length) return null;
|
|
const rows: Array<[KeyValueItem, KeyValueItem | null]> = [];
|
|
for (let i = 0; i < filtered.length; i += 2) rows.push([filtered[i], filtered[i + 1] || null]);
|
|
|
|
return (
|
|
<View style={styles.kvGrid}>
|
|
{rows.map(([left, right], rowIndex) => {
|
|
const isLast = rowIndex === rows.length - 1;
|
|
const leftValue = left.unit ? `${left.value} ${left.unit}` : left.value;
|
|
const rightValue = right ? (right.unit ? `${right.value} ${right.unit}` : right.value) : '';
|
|
return (
|
|
<View
|
|
key={`${left.label}-${rowIndex}`}
|
|
style={[
|
|
styles.kvRow,
|
|
rowIndex % 2 === 0 ? styles.kvRowAlt : null,
|
|
isLast ? styles.kvRowLast : null,
|
|
]}
|
|
wrap={false}
|
|
>
|
|
<View style={[styles.kvCell, { width: '23%' }]}>
|
|
<Text style={styles.kvLabelText}>{left.label}</Text>
|
|
</View>
|
|
<View style={[styles.kvCell, styles.kvMidDivider, { width: '27%' }]}>
|
|
<Text style={styles.kvValueText}>{leftValue}</Text>
|
|
</View>
|
|
<View style={[styles.kvCell, { width: '23%' }]}>
|
|
<Text style={styles.kvLabelText}>{right?.label || ''}</Text>
|
|
</View>
|
|
<View style={[styles.kvCell, { width: '27%' }]}>
|
|
<Text style={styles.kvValueText}>{rightValue}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function DenseTable({
|
|
table,
|
|
firstColLabel,
|
|
}: {
|
|
table: Pick<DatasheetVoltageTable, 'columns' | 'rows'>;
|
|
firstColLabel: string;
|
|
}) {
|
|
const cols = table.columns;
|
|
const rows = table.rows;
|
|
const headerText = (label: string) =>
|
|
String(label || '')
|
|
.replace(/\s+/g, '\u00A0')
|
|
.trim();
|
|
|
|
const cfgMin = 0.14,
|
|
cfgMax = 0.23;
|
|
const cfgContentLen = Math.max(
|
|
textLen(firstColLabel),
|
|
...rows.map((r) => textLen(r.configuration)),
|
|
8,
|
|
);
|
|
const dataContentLens = cols.map((c, ci) => {
|
|
const headerL = textLen(c.label);
|
|
let cellMax = 0;
|
|
for (const r of rows) cellMax = Math.max(cellMax, textLen(r.cells[ci]));
|
|
return Math.max(headerL * 1.15, cellMax, 3);
|
|
});
|
|
|
|
const cfgWeight = cfgContentLen * 1.05;
|
|
const dataWeights = dataContentLens.map((l) => l);
|
|
const dataWeightSum = dataWeights.reduce((a, b) => a + b, 0);
|
|
const rawCfgPct = dataWeightSum > 0 ? cfgWeight / (cfgWeight + dataWeightSum) : 0.28;
|
|
let cfgPct = clamp(rawCfgPct, cfgMin, cfgMax);
|
|
|
|
const minDataPct =
|
|
cols.length >= 14 ? 0.045 : cols.length >= 12 ? 0.05 : cols.length >= 10 ? 0.055 : 0.06;
|
|
const cfgPctMaxForMinData = 1 - cols.length * minDataPct;
|
|
if (Number.isFinite(cfgPctMaxForMinData)) cfgPct = Math.min(cfgPct, cfgPctMaxForMinData);
|
|
cfgPct = clamp(cfgPct, cfgMin, cfgMax);
|
|
|
|
const dataTotal = Math.max(0, 1 - cfgPct);
|
|
const maxDataPct = Math.min(0.24, Math.max(minDataPct * 2.8, dataTotal * 0.55));
|
|
const dataPcts = distributeWithMinMax(dataWeights, dataTotal, minDataPct, maxDataPct);
|
|
|
|
const cfgW = `${(cfgPct * 100).toFixed(4)}%`;
|
|
const dataWs = dataPcts.map((p, idx) => {
|
|
if (idx === dataPcts.length - 1) {
|
|
const used = dataPcts.slice(0, -1).reduce((a, b) => a + b, 0);
|
|
const remainder = Math.max(0, dataTotal - used);
|
|
return `${(remainder * 100).toFixed(4)}%`;
|
|
}
|
|
return `${(p * 100).toFixed(4)}%`;
|
|
});
|
|
|
|
const headerFontSize =
|
|
cols.length >= 14 ? 5.7 : cols.length >= 12 ? 5.9 : cols.length >= 10 ? 6.2 : 6.6;
|
|
|
|
return (
|
|
<View style={styles.tableWrap} break={false}>
|
|
<View style={styles.tableHeader} wrap={false}>
|
|
<View style={{ width: cfgW }}>
|
|
<Text
|
|
style={[
|
|
styles.tableHeaderCell,
|
|
styles.tableHeaderCellCfg,
|
|
{ fontSize: headerFontSize, paddingHorizontal: 3 },
|
|
cols.length ? styles.tableHeaderCellDivider : null,
|
|
]}
|
|
wrap={false}
|
|
>
|
|
{headerText(firstColLabel)}
|
|
</Text>
|
|
</View>
|
|
{cols.map((c, idx) => {
|
|
const isLast = idx === cols.length - 1;
|
|
return (
|
|
<View key={c.key} style={{ width: dataWs[idx] }}>
|
|
<Text
|
|
style={[
|
|
styles.tableHeaderCell,
|
|
{ fontSize: headerFontSize, paddingHorizontal: 3 },
|
|
!isLast ? styles.tableHeaderCellDivider : null,
|
|
]}
|
|
wrap={false}
|
|
>
|
|
{headerText(c.label)}
|
|
</Text>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
{rows.map((r, ri) => (
|
|
<View
|
|
key={`${r.configuration}-${ri}`}
|
|
style={[styles.tableRow, ri % 2 === 0 ? styles.tableRowAlt : null]}
|
|
wrap={false}
|
|
minPresenceAhead={16}
|
|
>
|
|
<View style={{ width: cfgW }} wrap={false}>
|
|
<Text
|
|
style={[
|
|
styles.tableCell,
|
|
styles.tableCellCfg,
|
|
{ fontSize: 6.2, paddingHorizontal: 3 },
|
|
cols.length ? styles.tableCellDivider : null,
|
|
]}
|
|
wrap={false}
|
|
>
|
|
{r.configuration}
|
|
</Text>
|
|
</View>
|
|
{r.cells.map((cell, ci) => (
|
|
<View key={`${cols[ci]?.key || ci}`} style={{ width: dataWs[ci] }} wrap={false}>
|
|
<Text
|
|
style={[
|
|
styles.tableCell,
|
|
ci !== r.cells.length - 1 ? styles.tableCellDivider : null,
|
|
]}
|
|
wrap={false}
|
|
>
|
|
{cell}
|
|
</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
|
|
product,
|
|
locale,
|
|
technicalItems = [],
|
|
voltageTables = [],
|
|
legendItems = [],
|
|
}) => {
|
|
const labels = getLabels(locale);
|
|
const description = stripHtml(
|
|
product.applicationHtml ||
|
|
product.shortDescriptionHtml ||
|
|
product.descriptionHtml ||
|
|
product.descriptionText ||
|
|
'',
|
|
);
|
|
|
|
return (
|
|
<Document>
|
|
<Page size="A4" style={styles.page}>
|
|
<View style={styles.hero}>
|
|
<View style={styles.header}>
|
|
<View style={{ width: 80 }}>
|
|
{product.logoDataUrl || (product as any).logoDataUrl ? (
|
|
<Image
|
|
src={product.logoDataUrl || (product as any).logoDataUrl}
|
|
style={{ width: '100%', objectFit: 'contain' }}
|
|
/>
|
|
) : (
|
|
<Text style={styles.logoText}>KLZ</Text>
|
|
)}
|
|
</View>
|
|
<Text style={styles.docTitle}>{labels.productDatasheet}</Text>
|
|
</View>
|
|
<View style={styles.productRow}>
|
|
<View style={styles.productInfoCol}>
|
|
<Text style={styles.productMeta}>
|
|
{product.categoriesLine ||
|
|
(product.categories || []).map((c) => c.name).join(' • ')}
|
|
</Text>
|
|
<Text style={styles.productName}>{product.name}</Text>
|
|
</View>
|
|
<View style={styles.productImageCol}>
|
|
{product.featuredImage ? (
|
|
<Image src={product.featuredImage} style={styles.heroImage} />
|
|
) : (
|
|
<Text style={styles.noImage}>{labels.noImage}</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.content}>
|
|
{description && (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>{labels.description}</Text>
|
|
<View style={styles.sectionAccent} />
|
|
<Text style={styles.description}>{description}</Text>
|
|
</View>
|
|
)}
|
|
|
|
{technicalItems.length > 0 && (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
|
|
<View style={styles.sectionAccent} />
|
|
<KeyValueGrid items={technicalItems} />
|
|
</View>
|
|
)}
|
|
|
|
{voltageTables.map((table, idx) => (
|
|
<View key={idx} style={styles.section} break={false}>
|
|
<Text
|
|
style={styles.sectionTitle}
|
|
>{`${labels.crossSection} — ${table.voltageLabel}`}</Text>
|
|
<View style={styles.sectionAccent} />
|
|
<DenseTable table={table} firstColLabel={labels.slug_cs} />
|
|
</View>
|
|
))}
|
|
|
|
{legendItems.length > 0 && (
|
|
<View style={styles.section} break={false}>
|
|
<Text style={styles.sectionTitle}>{labels.abbreviations}</Text>
|
|
<View style={styles.sectionAccent} />
|
|
<KeyValueGrid items={legendItems} />
|
|
</View>
|
|
)}
|
|
|
|
{!technicalItems.length &&
|
|
!voltageTables.length &&
|
|
product.attributes &&
|
|
product.attributes.length > 0 && (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
|
|
<View style={styles.sectionAccent} />
|
|
<View style={{ borderWidth: 1, borderColor: C.gray200 }}>
|
|
{product.attributes.map((attr, index) => (
|
|
<View
|
|
key={index}
|
|
style={{
|
|
flexDirection: 'row',
|
|
borderBottomWidth: index === product.attributes!.length - 1 ? 0 : 1,
|
|
borderBottomColor: C.gray200,
|
|
backgroundColor: index % 2 === 0 ? C.offWhite : C.white,
|
|
}}
|
|
>
|
|
<View
|
|
style={{
|
|
flex: 1,
|
|
padding: 6,
|
|
borderRightWidth: 1,
|
|
borderRightColor: C.gray200,
|
|
}}
|
|
>
|
|
<Text style={{ fontSize: 8.5, fontWeight: 700, color: C.gray600 }}>
|
|
{attr.name}
|
|
</Text>
|
|
</View>
|
|
<View style={{ flex: 1, padding: 6 }}>
|
|
<Text style={{ fontSize: 9.5, color: C.gray900 }}>
|
|
{attr.options.join(', ')}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
<View style={styles.footer} fixed>
|
|
<View style={{ width: 60 }}>
|
|
{product.logoDataUrl || (product as any).logoDataUrl ? (
|
|
<Image
|
|
src={product.logoDataUrl || (product as any).logoDataUrl}
|
|
style={{ width: '100%', objectFit: 'contain' }}
|
|
/>
|
|
) : (
|
|
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
|
)}
|
|
</View>
|
|
<Text style={styles.footerText}>
|
|
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
})}
|
|
</Text>
|
|
</View>
|
|
</Page>
|
|
</Document>
|
|
);
|
|
};
|