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>
This commit is contained in:
883
EXPORT_FUNCTIONALITY_PLAN.md
Normal file
883
EXPORT_FUNCTIONALITY_PLAN.md
Normal file
@@ -0,0 +1,883 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user