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>
21 KiB
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
- Export Bible passages in multiple formats (PDF, DOCX, Markdown, TXT)
- Include user highlights and notes in exports
- Provide print-optimized layouts
- Support batch exports (multiple chapters/books)
- 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
- Beta: Limited users, PDF only
- Staged: 50% users, all formats
- Full: 100% deployment
Document Version: 1.0 Last Updated: 2025-10-13 Owner: Development Team Status: Ready for Implementation