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

884 lines
21 KiB
Markdown

# 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
```typescript
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
```typescript
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)
```typescript
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)
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
// 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