bookwiz.io / lib / utils / download-utils.ts
download-utils.ts
Raw
import { FileSystemItem } from '@/lib/types/database'
import { Editor } from '@tiptap/react'

export interface DownloadOptions {
  format: 'current' | 'pdf'
  fileName: string
  content: string
  fileExtension?: string
}

/**
 * Downloads a file in its current format
 */
export function downloadCurrentFormat(file: FileSystemItem): void {
  if (!file.content) {
    console.error('No content to download')
    return
  }

  const blob = new Blob([file.content], { 
    type: getContentType(file.file_extension || 'txt') 
  })
  
  const url = URL.createObjectURL(blob)
  const link = document.createElement('a')
  link.href = url
  link.download = file.name
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
  URL.revokeObjectURL(url)
}

/**
 * Downloads a file as PDF using TipTap editor instance
 * This provides the best formatting since it uses the actual rendered HTML
 */
export async function downloadAsPDFFromEditor(
  editor: Editor,
  fileName: string
): Promise<void> {
  try {
    // Get the HTML content from TipTap editor
    const htmlContent = editor.getHTML()
    
    // Create a complete HTML document with proper styling
    const fullHtml = createPrintableHTML(htmlContent, fileName)
    
    // Use the browser's print API with PDF generation
    await generatePDFFromHTML(fullHtml, fileName)
    
  } catch (error) {
    console.error('Error generating PDF from editor:', error)
    throw new Error('Failed to generate PDF from editor')
  }
}

/**
 * Downloads a file as PDF (fallback method)
 * This is used when TipTap editor instance is not available
 */
export async function downloadAsPDF(file: FileSystemItem): Promise<void> {
  if (!file.content) {
    console.error('No content to download')
    return
  }

  try {
    // Create a temporary editor instance to convert markdown to HTML
    const { Editor } = await import('@tiptap/core')
    const { default: StarterKit } = await import('@tiptap/starter-kit')
    
    const editor = new Editor({
      extensions: [StarterKit],
      content: file.content,
    })
    
    const htmlContent = editor.getHTML()
    const fileName = file.name.replace(/\.[^/.]+$/, '')
    
    // Create a complete HTML document
    const fullHtml = createPrintableHTML(htmlContent, fileName)
    
    // Generate PDF
    await generatePDFFromHTML(fullHtml, fileName + '.pdf')
    
  } catch (error) {
    console.error('Error generating PDF:', error)
    // Fallback to plain text download
    downloadCurrentFormat(file)
  }
}

/**
 * Create a complete HTML document optimized for PDF generation
 */
function createPrintableHTML(content: string, title: string): string {
  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${escapeHtml(title)}</title>
  <style>
    /* Reset and base styles */
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    @page {
      size: A4;
      margin: 2cm;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
      font-size: 12pt;
      line-height: 1.6;
      color: #333;
      background: white;
      max-width: none;
    }
    
    /* Typography */
    h1, h2, h3, h4, h5, h6 {
      font-weight: 600;
      margin-top: 1.5em;
      margin-bottom: 0.5em;
      page-break-after: avoid;
    }
    
    h1 {
      font-size: 24pt;
      border-bottom: 2px solid #333;
      padding-bottom: 0.3em;
      margin-bottom: 1em;
    }
    
    h2 {
      font-size: 20pt;
      border-bottom: 1px solid #ddd;
      padding-bottom: 0.2em;
    }
    
    h3 { font-size: 16pt; }
    h4 { font-size: 14pt; }
    h5 { font-size: 12pt; }
    h6 { font-size: 11pt; }
    
    p {
      margin-bottom: 1em;
      orphans: 3;
      widows: 3;
    }
    
    /* Lists */
    ul, ol {
      margin: 0.5em 0 1em 1.5em;
    }
    
    li {
      margin-bottom: 0.3em;
      page-break-inside: avoid;
    }
    
    /* Task lists */
    li[data-type="taskItem"] {
      list-style: none;
      margin-left: -1.5em;
    }
    
    li[data-type="taskItem"]:before {
      content: "☐ ";
      margin-right: 0.5em;
    }
    
    li[data-type="taskItem"][data-checked="true"]:before {
      content: "☑ ";
    }
    
    /* Code */
    code {
      background: #f5f5f5;
      padding: 0.1em 0.3em;
      border-radius: 3px;
      font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
      font-size: 0.9em;
    }
    
    pre {
      background: #f8f8f8;
      border: 1px solid #ddd;
      border-radius: 4px;
      padding: 1em;
      margin: 1em 0;
      overflow-x: auto;
      page-break-inside: avoid;
    }
    
    pre code {
      background: none;
      padding: 0;
      border-radius: 0;
    }
    
    /* Blockquotes */
    blockquote {
      border-left: 4px solid #ddd;
      padding-left: 1em;
      margin: 1em 0;
      color: #666;
      font-style: italic;
      page-break-inside: avoid;
    }
    
    /* Tables */
    table {
      width: 100%;
      border-collapse: collapse;
      margin: 1em 0;
      page-break-inside: avoid;
    }
    
    th, td {
      border: 1px solid #ddd;
      padding: 0.5em;
      text-align: left;
    }
    
    th {
      background: #f5f5f5;
      font-weight: 600;
    }
    
    tbody tr:nth-child(even) {
      background: #f9f9f9;
    }
    
    /* Links */
    a {
      color: #0066cc;
      text-decoration: none;
    }
    
    a:after {
      content: " (" attr(href) ")";
      font-size: 0.8em;
      color: #666;
    }
    
    /* Images */
    img {
      max-width: 100%;
      height: auto;
      display: block;
      margin: 1em auto;
      page-break-inside: avoid;
    }
    
    /* Horizontal rules */
    hr {
      border: none;
      border-top: 2px solid #ddd;
      margin: 2em 0;
      page-break-after: avoid;
    }
    
    /* Page breaks */
    .page-break {
      page-break-before: always;
      break-before: page;
    }
    
    /* Utility classes */
    .no-break {
      page-break-inside: avoid;
    }
    
    /* Print-specific */
    @media print {
      body {
        font-size: 11pt;
      }
      
      a:after {
        font-size: 9pt;
      }
      
      .no-print {
        display: none !important;
      }
      
      /* Ensure proper page margins in print */
      @page :first {
        margin-top: 3cm;
      }
    }
  </style>
</head>
<body>
  <div class="document-content">
    ${content}
  </div>
</body>
</html>`
}

/**
 * Generate PDF from HTML using the browser's print API
 */
async function generatePDFFromHTML(html: string, fileName: string): Promise<void> {
  // Create a new window for printing
  const printWindow = window.open('', '_blank', 'width=800,height=600')
  
  if (!printWindow) {
    throw new Error('Could not open print window. Please allow popups for this site.')
  }
  
  // Write HTML to the new window
  printWindow.document.write(html)
  printWindow.document.close()
  
  // Wait for the content to load
  await new Promise<void>((resolve) => {
    printWindow.onload = () => {
      // Small delay to ensure everything is rendered
      setTimeout(() => {
        resolve()
      }, 100)
    }
  })
  
  // Set the document title for the PDF
  printWindow.document.title = fileName
  
  // Trigger print dialog
  printWindow.print()
  
  // Clean up after a delay
  setTimeout(() => {
    printWindow.close()
  }, 1000)
}

/**
 * Alternative PDF generation using Puppeteer (for server-side)
 * This would be called via an API endpoint
 */
export async function generatePDFServerSide(
  content: string,
  fileName: string
): Promise<Blob> {
  const response = await fetch('/api/generate-pdf', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      html: createPrintableHTML(content, fileName),
      fileName
    })
  })
  
  if (!response.ok) {
    throw new Error('Failed to generate PDF on server')
  }
  
  return response.blob()
}

/**
 * Escape HTML special characters
 */
function escapeHtml(text: string): string {
  const div = document.createElement('div')
  div.textContent = text
  return div.innerHTML
}

/**
 * Get the appropriate MIME type for a file extension
 */
function getContentType(extension: string): string {
  const ext = extension.toLowerCase()
  
  const mimeTypes: Record<string, string> = {
    'txt': 'text/plain',
    'md': 'text/markdown',
    'markdown': 'text/markdown',
    'html': 'text/html',
    'htm': 'text/html',
    'json': 'application/json',
    'js': 'text/javascript',
    'ts': 'text/typescript',
    'css': 'text/css',
    'xml': 'text/xml',
    'csv': 'text/csv'
  }
  
  return mimeTypes[ext] || 'text/plain'
}

/**
 * Get a user-friendly format name for display
 */
export function getFormatDisplayName(extension?: string): string {
  if (!extension) return 'Text File'
  
  const ext = extension.toLowerCase()
  const displayNames: Record<string, string> = {
    'txt': 'Text File',
    'md': 'Markdown',
    'markdown': 'Markdown',
    'html': 'HTML',
    'htm': 'HTML',
    'json': 'JSON',
    'js': 'JavaScript',
    'ts': 'TypeScript',
    'css': 'CSS',
    'xml': 'XML',
    'csv': 'CSV'
  }
  
  return displayNames[ext] || ext.toUpperCase()
}

/**
 * Get a description of supported PDF features
 */
export function getPDFFeatures(): string[] {
  return [
    '✓ Perfect formatting using TipTap\'s rendered HTML',
    '✓ Proper page breaks and print styling',
    '✓ All markdown elements: headings, lists, tables, quotes',
    '✓ Syntax-highlighted code blocks',
    '✓ Clickable links with URLs shown in print',
    '✓ Task lists with checkboxes',
    '✓ Professional typography and spacing',
    '✓ Responsive images and media',
    '✓ Print-optimized styling',
    '✓ Browser-native PDF generation (no external dependencies)'
  ]
}