Skip to content

PDF Export System

Overview

The PDF export system provides a flexible, template-based architecture for generating PDF documents from application data. Built on top of @react-pdf/renderer, it supports custom templates, internationalization, and various formatting options.

Architecture

packages/app/src/lib/pdfExporter/
├── core/                 # Core PDF components and styles
│   ├── PdfDocument.tsx   # Base document wrapper
│   ├── PdfPage.tsx       # Page component with margins
│   └── PdfStyles.ts      # Shared styles and theme
├── components/           # Reusable PDF components
│   ├── PdfHeader.tsx     # Header with title/subtitle
│   ├── PdfSection.tsx    # Section with title
│   ├── PdfTable.tsx      # Table component
│   └── PdfText.tsx       # Text with styling options
├── utils/                # Utility functions
│   ├── formatters.ts     # Date, currency, number formatting
│   └── generatePdf.ts    # PDF generation and download
├── hooks/                # React hooks
│   └── usePdfExport.ts   # Export hook with progress
├── templates/            # PDF templates
│   └── DataTableTemplate.tsx  # DataTable export template
└── types.ts              # TypeScript definitions

Quick Start

Basic Usage

import { usePdfExport, DataTableTemplate } from '@/lib/pdfExporter';

function MyComponent() {
  const { exportPdf, isExporting, progress, error } = usePdfExport();

  const handleExport = async () => {
    await exportPdf({
      template: DataTableTemplate,
      data: { table, title: 'My Report' },
      filename: 'report.pdf',
      options: {
        locale: 'en-US',
      },
    });
  };

  return (
    <button onClick={handleExport} disabled={isExporting}>
      {isExporting ? `Exporting... ${progress}%` : 'Export PDF'}
    </button>
  );
}

DataTable Export

For DataTable components, use the specialized hook:

import { useDataTablePdfExport } from '@/components/ui/DataTable/utils/export/pdfExporter';

function DataTableComponent() {
  const { exportDataTable, isExporting } = useDataTablePdfExport();

  const handleExport = async () => {
    await exportDataTable(table, {
      filename: 'data-export.pdf',
      title: 'Sales Report',
      companyName: 'Acme Corp',
      includeDate: true,
      includeFooter: true,
      visibleColumnsOnly: true,
      exportSelection: EXPORT_SELECTION.ALL,
      locale: 'en-US',
    });
  };

  return (
    <button onClick={handleExport} disabled={isExporting}>
      Export to PDF
    </button>
  );
}

Creating Custom Templates

Template Structure

A PDF template is a React component that returns @react-pdf/renderer elements:

import { ReactElement } from 'react';
import { View, Text } from '@react-pdf/renderer';
import {
  PdfDocument,
  PdfPage,
  PdfHeader,
  PdfSection,
  PdfTable,
  PdfText,
  type PdfTemplate,
  type PdfDocumentConfig,
} from '@/lib/pdfExporter';

interface MyTemplateData {
  title: string;
  items: Array<{ name: string; value: number }>;
  date: Date;
}

function renderMyTemplate(
  data: MyTemplateData,
  config: PdfDocumentConfig
): ReactElement {
  const { locale = 'en-US', currency = 'USD' } = config;

  return (
    <PdfDocument title={data.title}>
      <PdfPage>
        <PdfHeader title={data.title} />

        <PdfSection title="Summary">
          <PdfText label="Date">
            {data.date.toLocaleDateString(locale)}
          </PdfText>
        </PdfSection>

        <PdfTable
          headers={['Item', 'Value']}
          rows={data.items.map(item => [
            item.name,
            formatPdfCurrency(item.value, currency, locale),
          ])}
          alignments={['left', 'right']}
        />
      </PdfPage>
    </PdfDocument>
  );
}

export const MyTemplate: PdfTemplate = {
  name: 'MyTemplate',
  render: renderMyTemplate as (data: unknown, config: PdfDocumentConfig) => ReactElement,
};

Using Custom Templates

import { usePdfExport } from '@/lib/pdfExporter';
import { MyTemplate } from './templates/MyTemplate';

function ExportButton() {
  const { exportPdf } = usePdfExport();

  const handleExport = async () => {
    const data = {
      title: 'Monthly Report',
      items: [
        { name: 'Product A', value: 100 },
        { name: 'Product B', value: 200 },
      ],
      date: new Date(),
    };

    await exportPdf({
      template: MyTemplate,
      data,
      filename: 'monthly-report.pdf',
      options: {
        locale: 'en-US',
      },
    });
  };

  return <button onClick={handleExport}>Export Report</button>;
}

Component API

Core Components

PdfDocument

Base document wrapper with metadata.

<PdfDocument
  title="Report Title"
  author="John Doe"
  subject="Monthly Sales"
  keywords="sales, report, monthly"
>
  {/* pages */}
</PdfDocument>

PdfPage

Page component with configurable margins.

<PdfPage margin={20}>
  {/* content */}
</PdfPage>

Layout Components

PdfHeader

Header with title and optional subtitle.

<PdfHeader
  title="Main Title"
  subtitle="Subtitle text"
/>

PdfSection

Section with title and content.

<PdfSection title="Section Title">
  {/* section content */}
</PdfSection>

Content Components

PdfText

Text component with styling options.

<PdfText
  label="Field Label"
  bold={true}
  muted={false}
  small={false}
  large={false}
>
  Text content
</PdfText>

PdfTable

Table component with headers and rows.

<PdfTable
  headers={['Column 1', 'Column 2', 'Column 3']}
  rows={[
    ['Row 1 Col 1', 'Row 1 Col 2', 'Row 1 Col 3'],
    ['Row 2 Col 1', 'Row 2 Col 2', 'Row 2 Col 3'],
  ]}
  alignments={['left', 'center', 'right']}
  columnWidths={[100, 150, 100]}
/>

Utility Functions

Formatters

import {
  formatPdfCurrency,
  formatPdfDate,
  formatPdfNumber,
  formatPdfPercentage,
} from '@/lib/pdfExporter/utils';

// Currency formatting
formatPdfCurrency(1234.56, 'USD', 'en-US'); // "$1,234.56"
formatPdfCurrency(1234.56, 'EUR', 'de-DE'); // "1.234,56 €"

// Date formatting
formatPdfDate(new Date(), 'en-US'); // "1/15/2024"
formatPdfDate(new Date(), 'de-DE'); // "15.1.2024"

// Number formatting
formatPdfNumber(1234.56, 'en-US'); // "1,234.56"
formatPdfNumber(1234.56, 'de-DE'); // "1.234,56"

// Percentage formatting
formatPdfPercentage(0.85, 'en-US'); // "85%"

PDF Generation

import {
  generatePdf,
  downloadPdf,
  generateAndDownloadPdf,
} from '@/lib/pdfExporter/utils';

// Generate PDF blob
const blob = await generatePdf(document, {
  onProgress: (progress) => console.log(`Progress: ${progress * 100}%`),
  onComplete: () => console.log('Complete'),
  onError: (error) => console.error('Error:', error),
});

// Download blob as PDF
downloadPdf(blob, 'document.pdf');

// Generate and download in one step
await generateAndDownloadPdf(document, 'document.pdf');

Hooks

usePdfExport

Main export hook with progress tracking.

const {
  exportPdf, // Export function
  generatePdfBlob, // Generate blob without downloading
  isExporting, // Loading state
  progress, // Progress (0-100)
  error, // Error state
} = usePdfExport();

Styling

Theme Configuration

Customize the default theme in PdfStyles.ts:

export const defaultTheme = {
  colors: {
    primary: '#000000',
    secondary: '#666666',
    muted: '#999999',
    border: '#e5e7eb',
    background: '#f9fafb',
  },
  fontSize: {
    small: 8,
    normal: 10,
    medium: 12,
    large: 14,
    xlarge: 16,
    title: 20,
  },
  spacing: {
    xs: 2,
    sm: 4,
    md: 8,
    lg: 12,
    xl: 16,
  },
};

Custom Styles

Create custom styles using the createStyles function:

import { createStyles } from '@/lib/pdfExporter/core';

const customStyles = createStyles({
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
  },
  fontSize: {
    normal: 11,
    large: 15,
  },
});

Internationalization

The system supports full internationalization for dates, numbers, and currency:

await exportPdf({
  template: MyTemplate,
  data: myData,
  filename: 'report.pdf',
  options: {
    locale: 'fr-FR', // French locale
    currency: 'EUR', // Euro currency
  },
});

Migration from jsPDF

If you're migrating from the old jsPDF-based system:

  1. DataTable exports are automatically compatible - no changes needed
  2. Custom exports need to be converted to templates:
// Old jsPDF approach
const doc = new jsPDF();
doc.text('Hello World', 10, 10);
doc.save('document.pdf');

// New template approach
const SimpleTemplate: PdfTemplate = {
  name: 'Simple',
  render: (data) => (
    <PdfDocument>
      <PdfPage>
        <PdfText>Hello World</PdfText>
      </PdfPage>
    </PdfDocument>
  ),
};

await exportPdf({
  template: SimpleTemplate,
  data: {},
  filename: 'document.pdf',
});

Best Practices

  1. Template Organization: Keep templates in /lib/pdfExporter/templates/
  2. Type Safety: Always type your template data interfaces
  3. Internationalization: Use locale-aware formatters for dates and numbers
  4. Performance: For large documents, consider pagination
  5. Error Handling: Always handle export errors in your UI
  6. Progress Feedback: Show progress for better UX
  7. Accessibility: Provide alternative formats when possible

Examples

Invoice Template

interface InvoiceData {
  invoiceNumber: string;
  date: Date;
  customer: {
    name: string;
    address: string;
  };
  items: Array<{
    description: string;
    quantity: number;
    price: number;
  }>;
}

function renderInvoice(data: InvoiceData, config: PdfDocumentConfig): ReactElement {
  const { locale, currency } = config;
  const total = data.items.reduce((sum, item) => sum + item.quantity * item.price, 0);

  return (
    <PdfDocument title={`Invoice ${data.invoiceNumber}`}>
      <PdfPage>
        <PdfHeader title={`Invoice #${data.invoiceNumber}`} />

        <PdfSection title="Customer">
          <PdfText>{data.customer.name}</PdfText>
          <PdfText muted>{data.customer.address}</PdfText>
        </PdfSection>

        <PdfSection title="Items">
          <PdfTable
            headers={['Description', 'Qty', 'Price', 'Total']}
            rows={data.items.map(item => [
              item.description,
              String(item.quantity),
              formatPdfCurrency(item.price, currency, locale),
              formatPdfCurrency(item.quantity * item.price, currency, locale),
            ])}
            alignments={['left', 'center', 'right', 'right']}
          />
        </PdfSection>

        <PdfText bold large>
          Total: {formatPdfCurrency(total, currency, locale)}
        </PdfText>
      </PdfPage>
    </PdfDocument>
  );
}

export const InvoiceTemplate: PdfTemplate = {
  name: 'Invoice',
  render: renderInvoice as (data: unknown, config: PdfDocumentConfig) => ReactElement,
};

Troubleshooting

Common Issues

  1. Fonts not displaying correctly
  2. The system uses built-in fonts from @react-pdf/renderer
  3. Custom fonts can be registered if needed

  4. Large file sizes

  5. Optimize images before including them
  6. Consider compression options

  7. Performance issues

  8. Use pagination for large datasets
  9. Implement virtual scrolling for preview

  10. TypeScript errors

  11. Ensure all template data is properly typed
  12. Cast template render functions as shown in examples

Future Enhancements

  • [ ] Add support for images and charts
  • [ ] Implement PDF preview component
  • [ ] Add watermark support
  • [ ] Support for digital signatures
  • [ ] Advanced layout options (columns, grids)
  • [ ] PDF/A compliance for archival