Files
biblical-guide.com/EXPORT_FUNCTIONALITY_PLAN.md
Andrei 9b5c0ed8bb build: production build with Phase 1 2025 Bible Reader implementation complete
Includes all Phase 1 features:
- Search-first navigation with auto-complete
- Responsive reading interface (desktop/tablet/mobile)
- 4 customization presets + full fine-tuning controls
- Layered details panel with notes, bookmarks, highlights
- Smart offline caching with IndexedDB and auto-sync
- Full accessibility (WCAG 2.1 AA)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 20:38:01 +00:00

21 KiB

Export Functionality - Implementation Plan

📋 Overview

Implement comprehensive export capabilities allowing users to download Bible passages, study notes, highlights, and annotations in multiple formats for offline study, sharing, and printing.

Status: Planning Phase Priority: 🔴 High Estimated Time: 2-3 weeks (80-120 hours) Target Completion: TBD


🎯 Goals & Objectives

Primary Goals

  1. Export Bible passages in multiple formats (PDF, DOCX, Markdown, TXT)
  2. Include user highlights and notes in exports
  3. Provide print-optimized layouts
  4. Support batch exports (multiple chapters/books)
  5. Enable customization of export appearance

User Value Proposition

  • For students: Create study materials for offline use
  • For teachers: Prepare handouts and lesson materials
  • For preachers: Print sermon references
  • For small groups: Share study guides
  • For archiving: Backup personal annotations

Feature Specifications

1. Export Formats

type ExportFormat = 'pdf' | 'docx' | 'markdown' | 'txt' | 'epub' | 'json'

interface ExportConfig {
  // Format
  format: ExportFormat

  // Content selection
  book: string
  startChapter: number
  endChapter: number
  startVerse?: number
  endVerse?: number
  includeHeadings: boolean
  includeVerseNumbers: boolean
  includeChapterNumbers: boolean

  // User content
  includeHighlights: boolean
  includeNotes: boolean
  includeBookmarks: boolean
  notesPosition: 'inline' | 'footnotes' | 'endnotes' | 'separate'

  // Appearance
  fontSize: number // 10-16pt
  fontFamily: string
  lineHeight: number // 1.0-2.0
  pageSize: 'A4' | 'Letter' | 'Legal'
  margins: { top: number; right: number; bottom: number; left: number }
  columns: 1 | 2

  // Header/Footer
  includeHeader: boolean
  headerText: string
  includeFooter: boolean
  footerText: string
  includePageNumbers: boolean

  // Metadata
  includeTableOfContents: boolean
  includeCoverPage: boolean
  coverTitle: string
  coverSubtitle: string
  author: string
  date: string

  // Advanced
  versionComparison: string[] // Multiple version IDs for parallel
  colorMode: 'color' | 'grayscale' | 'print'
}

2. Export Dialog UI

const ExportDialog: React.FC<{
  open: boolean
  onClose: () => void
  defaultSelection?: {
    book: string
    chapter: number
  }
}> = ({ open, onClose, defaultSelection }) => {
  const [config, setConfig] = useState<ExportConfig>(getDefaultConfig())
  const [estimatedSize, setEstimatedSize] = useState<string>('0 KB')
  const [exporting, setExporting] = useState(false)
  const [progress, setProgress] = useState(0)

  // Calculate estimated file size
  useEffect(() => {
    const estimate = calculateEstimatedSize(config)
    setEstimatedSize(estimate)
  }, [config])

  const handleExport = async () => {
    setExporting(true)
    setProgress(0)

    try {
      const result = await exportContent(config, (percent) => {
        setProgress(percent)
      })

      // Trigger download
      downloadFile(result.blob, result.filename)

      onClose()
    } catch (error) {
      console.error('Export failed:', error)
      // Show error to user
    } finally {
      setExporting(false)
    }
  }

  return (
    <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
      <DialogTitle>
        Export Bible Content
      </DialogTitle>

      <DialogContent>
        <Box sx={{ pt: 2 }}>
          <Tabs value={activeTab} onChange={setActiveTab}>
            <Tab label="Content" />
            <Tab label="Format" />
            <Tab label="Layout" />
            <Tab label="Advanced" />
          </Tabs>

          <Box sx={{ mt: 3 }}>
            {activeTab === 0 && <ContentSelectionTab config={config} onChange={setConfig} />}
            {activeTab === 1 && <FormatOptionsTab config={config} onChange={setConfig} />}
            {activeTab === 2 && <LayoutSettingsTab config={config} onChange={setConfig} />}
            {activeTab === 3 && <AdvancedOptionsTab config={config} onChange={setConfig} />}
          </Box>

          {/* Preview */}
          <Box sx={{ mt: 3, p: 2, bgcolor: 'grey.100', borderRadius: 1 }}>
            <Typography variant="caption" color="text.secondary">
              Estimated file size: {estimatedSize}
            </Typography>
          </Box>

          {/* Progress */}
          {exporting && (
            <Box sx={{ mt: 2 }}>
              <LinearProgress variant="determinate" value={progress} />
              <Typography variant="caption" textAlign="center" display="block" mt={1}>
                Generating {config.format.toUpperCase()}... {progress}%
              </Typography>
            </Box>
          )}
        </Box>
      </DialogContent>

      <DialogActions>
        <Button onClick={onClose}>Cancel</Button>
        <Button
          variant="contained"
          onClick={handleExport}
          disabled={exporting}
          startIcon={<DownloadIcon />}
        >
          Export
        </Button>
      </DialogActions>
    </Dialog>
  )
}

3. PDF Export (using jsPDF)

import jsPDF from 'jspdf'
import 'jspdf-autotable'

export const generatePDF = async (
  config: ExportConfig,
  onProgress?: (percent: number) => void
): Promise<Blob> => {
  const doc = new jsPDF({
    orientation: config.columns === 2 ? 'landscape' : 'portrait',
    unit: 'mm',
    format: config.pageSize.toLowerCase()
  })

  // Set font
  doc.setFont(config.fontFamily)
  doc.setFontSize(config.fontSize)

  let currentPage = 1

  // Add cover page
  if (config.includeCoverPage) {
    addCoverPage(doc, config)
    doc.addPage()
    currentPage++
  }

  // Add table of contents
  if (config.includeTableOfContents) {
    const toc = await generateTableOfContents(config)
    addTableOfContents(doc, toc)
    doc.addPage()
    currentPage++
  }

  // Fetch Bible content
  const verses = await fetchVerses(
    config.book,
    config.startChapter,
    config.endChapter,
    config.startVerse,
    config.endVerse
  )

  const totalVerses = verses.length
  let processedVerses = 0

  // Group by chapters
  const chapters = groupByChapters(verses)

  for (const [chapterNum, chapterVerses] of Object.entries(chapters)) {
    // Chapter heading
    if (config.includeChapterNumbers) {
      doc.setFontSize(config.fontSize + 4)
      doc.setFont(config.fontFamily, 'bold')
      doc.text(`Chapter ${chapterNum}`, 20, doc.internal.pageSize.height - 20)
      doc.setFont(config.fontFamily, 'normal')
      doc.setFontSize(config.fontSize)
    }

    // Add verses
    for (const verse of chapterVerses) {
      const verseText = formatVerseForPDF(verse, config)

      // Check if we need a new page
      if (doc.internal.pageSize.height - 40 < 20) {
        doc.addPage()
        currentPage++
      }

      doc.text(verseText, 20, doc.internal.pageSize.height - 40)

      // Add highlights if enabled
      if (config.includeHighlights && verse.highlights) {
        addHighlightsToPDF(doc, verse.highlights)
      }

      // Add notes
      if (config.includeNotes && verse.notes) {
        if (config.notesPosition === 'inline') {
          addInlineNote(doc, verse.notes)
        } else if (config.notesPosition === 'footnotes') {
          addFootnote(doc, verse.notes, currentPage)
        }
      }

      processedVerses++
      if (onProgress) {
        onProgress(Math.round((processedVerses / totalVerses) * 100))
      }
    }
  }

  // Add header/footer to all pages
  if (config.includeHeader || config.includeFooter) {
    const totalPages = doc.getNumberOfPages()
    for (let i = 1; i <= totalPages; i++) {
      doc.setPage(i)

      if (config.includeHeader) {
        doc.setFontSize(10)
        doc.text(config.headerText, 20, 10)
      }

      if (config.includeFooter) {
        doc.setFontSize(10)
        const footerText = config.includePageNumbers
          ? `${config.footerText} | Page ${i} of ${totalPages}`
          : config.footerText
        doc.text(footerText, 20, doc.internal.pageSize.height - 10)
      }
    }
  }

  return doc.output('blob')
}

const formatVerseForPDF = (verse: BibleVerse, config: ExportConfig): string => {
  let text = ''

  if (config.includeVerseNumbers) {
    text += `${verse.verseNum}. `
  }

  text += verse.text

  return text
}

const addCoverPage = (doc: jsPDF, config: ExportConfig): void => {
  const pageWidth = doc.internal.pageSize.width
  const pageHeight = doc.internal.pageSize.height

  // Title
  doc.setFontSize(24)
  doc.setFont(config.fontFamily, 'bold')
  doc.text(config.coverTitle, pageWidth / 2, pageHeight / 2 - 20, { align: 'center' })

  // Subtitle
  doc.setFontSize(16)
  doc.setFont(config.fontFamily, 'normal')
  doc.text(config.coverSubtitle, pageWidth / 2, pageHeight / 2, { align: 'center' })

  // Author & Date
  doc.setFontSize(12)
  doc.text(config.author, pageWidth / 2, pageHeight / 2 + 30, { align: 'center' })
  doc.text(config.date, pageWidth / 2, pageHeight / 2 + 40, { align: 'center' })
}

4. DOCX Export (using docx library)

import { Document, Paragraph, TextRun, AlignmentType, HeadingLevel } from 'docx'
import { saveAs } from 'file-saver'
import { Packer } from 'docx'

export const generateDOCX = async (
  config: ExportConfig,
  onProgress?: (percent: number) => void
): Promise<Blob> => {
  const sections = []

  // Cover page
  if (config.includeCoverPage) {
    sections.push({
      children: [
        new Paragraph({
          text: config.coverTitle,
          heading: HeadingLevel.TITLE,
          alignment: AlignmentType.CENTER,
          spacing: { before: 400, after: 200 }
        }),
        new Paragraph({
          text: config.coverSubtitle,
          alignment: AlignmentType.CENTER,
          spacing: { after: 200 }
        }),
        new Paragraph({
          text: config.author,
          alignment: AlignmentType.CENTER,
          spacing: { after: 100 }
        }),
        new Paragraph({
          text: config.date,
          alignment: AlignmentType.CENTER
        })
      ]
    })
  }

  // Fetch content
  const verses = await fetchVerses(
    config.book,
    config.startChapter,
    config.endChapter
  )

  const chapters = groupByChapters(verses)

  for (const [chapterNum, chapterVerses] of Object.entries(chapters)) {
    // Chapter heading
    if (config.includeChapterNumbers) {
      sections.push(
        new Paragraph({
          text: `Chapter ${chapterNum}`,
          heading: HeadingLevel.HEADING_1,
          spacing: { before: 400, after: 200 }
        })
      )
    }

    // Verses
    for (const verse of chapterVerses) {
      const paragraph = new Paragraph({
        children: []
      })

      // Verse number
      if (config.includeVerseNumbers) {
        paragraph.addChildElement(
          new TextRun({
            text: `${verse.verseNum} `,
            bold: true
          })
        )
      }

      // Verse text
      paragraph.addChildElement(
        new TextRun({
          text: verse.text,
          size: config.fontSize * 2 // Convert to half-points
        })
      )

      sections.push(paragraph)

      // Highlights
      if (config.includeHighlights && verse.highlights) {
        for (const highlight of verse.highlights) {
          sections.push(
            new Paragraph({
              children: [
                new TextRun({
                  text: `[Highlight: ${highlight.color}] ${highlight.text}`,
                  italics: true,
                  color: highlight.color
                })
              ],
              spacing: { before: 100 }
            })
          )
        }
      }

      // Notes
      if (config.includeNotes && verse.notes) {
        sections.push(
          new Paragraph({
            children: [
              new TextRun({
                text: `Note: ${verse.notes}`,
                italics: true,
                color: '666666'
              })
            ],
            spacing: { before: 100, after: 100 }
          })
        )
      }
    }
  }

  const doc = new Document({
    sections: [{
      properties: {
        page: {
          margin: {
            top: config.margins.top * 56.7, // Convert mm to twips
            right: config.margins.right * 56.7,
            bottom: config.margins.bottom * 56.7,
            left: config.margins.left * 56.7
          }
        }
      },
      children: sections
    }]
  })

  return await Packer.toBlob(doc)
}

5. Markdown Export

export const generateMarkdown = async (
  config: ExportConfig
): Promise<string> => {
  let markdown = ''

  // Front matter
  if (config.includeCoverPage) {
    markdown += `---\n`
    markdown += `title: ${config.coverTitle}\n`
    markdown += `subtitle: ${config.coverSubtitle}\n`
    markdown += `author: ${config.author}\n`
    markdown += `date: ${config.date}\n`
    markdown += `---\n\n`
  }

  // Title
  markdown += `# ${config.coverTitle}\n\n`

  // Fetch content
  const verses = await fetchVerses(
    config.book,
    config.startChapter,
    config.endChapter
  )

  const chapters = groupByChapters(verses)

  for (const [chapterNum, chapterVerses] of Object.entries(chapters)) {
    // Chapter heading
    if (config.includeChapterNumbers) {
      markdown += `## Chapter ${chapterNum}\n\n`
    }

    // Verses
    for (const verse of chapterVerses) {
      if (config.includeVerseNumbers) {
        markdown += `**${verse.verseNum}** `
      }

      markdown += `${verse.text}\n\n`

      // Highlights
      if (config.includeHighlights && verse.highlights) {
        for (const highlight of verse.highlights) {
          markdown += `> 🎨 **Highlight (${highlight.color}):** ${highlight.text}\n\n`
        }
      }

      // Notes
      if (config.includeNotes && verse.notes) {
        markdown += `> 📝 **Note:** ${verse.notes}\n\n`
      }
    }

    markdown += '\n---\n\n'
  }

  return markdown
}

6. Batch Export

interface BatchExportConfig {
  books: string[]
  format: ExportFormat
  separate: boolean // Export each book as separate file
  combinedFilename?: string
}

export const batchExport = async (
  config: BatchExportConfig,
  onProgress?: (current: number, total: number) => void
): Promise<Blob | Blob[]> => {
  if (config.separate) {
    // Export each book separately
    const blobs: Blob[] = []

    for (let i = 0; i < config.books.length; i++) {
      const book = config.books[i]

      const exportConfig: ExportConfig = {
        ...getDefaultConfig(),
        book,
        startChapter: 1,
        endChapter: await getLastChapter(book),
        format: config.format
      }

      const blob = await exportContent(exportConfig)
      blobs.push(blob)

      if (onProgress) {
        onProgress(i + 1, config.books.length)
      }
    }

    return blobs
  } else {
    // Export all books in one file
    const exportConfig: ExportConfig = {
      ...getDefaultConfig(),
      format: config.format
      // Will loop through all books internally
    }

    return await exportContent(exportConfig)
  }
}

7. Print Optimization

const PrintPreview: React.FC<{
  config: ExportConfig
}> = ({ config }) => {
  const contentRef = useRef<HTMLDivElement>(null)

  const handlePrint = () => {
    const printWindow = window.open('', '', 'height=800,width=600')

    if (!printWindow) return

    const printStyles = `
      <style>
        @page {
          size: ${config.pageSize};
          margin: ${config.margins.top}mm ${config.margins.right}mm
                  ${config.margins.bottom}mm ${config.margins.left}mm;
        }

        body {
          font-family: ${config.fontFamily};
          font-size: ${config.fontSize}pt;
          line-height: ${config.lineHeight};
          color: ${config.colorMode === 'grayscale' ? '#000' : 'inherit'};
        }

        .verse-number {
          font-weight: bold;
          margin-right: 0.5em;
        }

        .chapter-heading {
          font-size: ${config.fontSize + 4}pt;
          font-weight: bold;
          margin-top: 2em;
          margin-bottom: 1em;
          break-before: page;
        }

        .highlight {
          background-color: ${config.colorMode === 'grayscale' ? '#ddd' : 'inherit'};
          padding: 0 2px;
        }

        .note {
          font-style: italic;
          color: #666;
          margin-left: 2em;
          margin-top: 0.5em;
        }

        @media print {
          .no-print {
            display: none;
          }
        }
      </style>
    `

    printWindow.document.write(`
      <!DOCTYPE html>
      <html>
        <head>
          <title>${config.coverTitle}</title>
          ${printStyles}
        </head>
        <body>
          ${contentRef.current?.innerHTML}
        </body>
      </html>
    `)

    printWindow.document.close()
    printWindow.focus()
    printWindow.print()
  }

  return (
    <Box>
      <Button onClick={handlePrint} startIcon={<PrintIcon />}>
        Print Preview
      </Button>

      <Box
        ref={contentRef}
        sx={{
          p: 3,
          bgcolor: 'white',
          minHeight: '100vh',
          fontFamily: config.fontFamily,
          fontSize: `${config.fontSize}pt`,
          lineHeight: config.lineHeight
        }}
      >
        {/* Rendered content here */}
      </Box>
    </Box>
  )
}

8. Email Export

interface EmailExportConfig {
  to: string[]
  subject: string
  message: string
  exportConfig: ExportConfig
}

const EmailExportDialog: React.FC = () => {
  const [config, setConfig] = useState<EmailExportConfig>({
    to: [],
    subject: '',
    message: '',
    exportConfig: getDefaultConfig()
  })

  const handleSend = async () => {
    // Generate export
    const blob = await exportContent(config.exportConfig)

    // Convert to base64
    const base64 = await blobToBase64(blob)

    // Send via API
    await fetch('/api/export/email', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        to: config.to,
        subject: config.subject,
        message: config.message,
        attachment: {
          filename: generateFilename(config.exportConfig),
          content: base64,
          contentType: getMimeType(config.exportConfig.format)
        }
      })
    })
  }

  return (
    <Dialog open={open} onClose={onClose}>
      <DialogTitle>Email Export</DialogTitle>
      <DialogContent>
        <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 2 }}>
          <TextField
            label="To"
            placeholder="email@example.com"
            value={config.to.join(', ')}
            onChange={(e) => setConfig({
              ...config,
              to: e.target.value.split(',').map(s => s.trim())
            })}
            fullWidth
          />
          <TextField
            label="Subject"
            value={config.subject}
            onChange={(e) => setConfig({ ...config, subject: e.target.value })}
            fullWidth
          />
          <TextField
            label="Message"
            value={config.message}
            onChange={(e) => setConfig({ ...config, message: e.target.value })}
            multiline
            rows={4}
            fullWidth
          />
        </Box>
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose}>Cancel</Button>
        <Button onClick={handleSend} variant="contained">
          Send
        </Button>
      </DialogActions>
    </Dialog>
  )
}

📊 API Endpoints

// Generate and download export
POST /api/export
Body: ExportConfig
Response: File (binary)

// Email export
POST /api/export/email
Body: {
  to: string[]
  subject: string
  message: string
  attachment: {
    filename: string
    content: string (base64)
    contentType: string
  }
}

// Get export templates
GET /api/export/templates
Response: { templates: ExportTemplate[] }

// Save export preset
POST /api/export/presets
Body: { name: string, config: ExportConfig }

📅 Implementation Timeline

Week 1: Core Export

Day 1-2: Foundation

  • Create export dialog UI
  • Build configuration forms
  • Implement content fetching

Day 3-4: PDF Export

  • Integrate jsPDF
  • Implement basic PDF generation
  • Add highlights/notes support
  • Test layouts

Day 5: DOCX & Markdown

  • Implement DOCX export
  • Implement Markdown export
  • Test formatting

Deliverable: Working PDF, DOCX, Markdown exports

Week 2: Advanced Features

Day 1-2: Layout Customization

  • Add cover page generation
  • Implement TOC
  • Add headers/footers
  • Build print preview

Day 3-4: Batch & Email

  • Implement batch export
  • Build email functionality
  • Add progress tracking
  • Test large exports

Day 5: Polish

  • Performance optimization
  • Error handling
  • UI refinement
  • Documentation

Deliverable: Production-ready export system


🚀 Deployment Plan

Pre-Launch

  • Test with various content sizes
  • Verify all formats generate correctly
  • Performance testing
  • Cross-browser testing
  • Mobile testing

Rollout

  1. Beta: Limited users, PDF only
  2. Staged: 50% users, all formats
  3. Full: 100% deployment

Document Version: 1.0 Last Updated: 2025-10-13 Owner: Development Team Status: Ready for Implementation