10 changed files with 398 additions and 20 deletions
@ -0,0 +1,162 @@
@@ -0,0 +1,162 @@
|
||||
import { Button } from '@/components/ui/button' |
||||
import { |
||||
DropdownMenu, |
||||
DropdownMenuContent, |
||||
DropdownMenuItem, |
||||
DropdownMenuTrigger, |
||||
} from '@/components/ui/dropdown-menu' |
||||
import { MoreVertical, FileDown } from 'lucide-react' |
||||
import { contentParserService } from '@/services/content-parser.service' |
||||
import logger from '@/lib/logger' |
||||
import { Event } from 'nostr-tools' |
||||
|
||||
interface ArticleExportMenuProps { |
||||
event: Event |
||||
title: string |
||||
} |
||||
|
||||
export default function ArticleExportMenu({ event, title }: ArticleExportMenuProps) { |
||||
const exportArticle = async (format: 'pdf' | 'epub' | 'latex' | 'adoc' | 'html') => { |
||||
try { |
||||
const content = event.content |
||||
const filename = `${title}.${format}` |
||||
|
||||
let blob: Blob = new Blob(['']) |
||||
|
||||
if (format === 'adoc') { |
||||
// Export raw AsciiDoc content
|
||||
blob = new Blob([content], { type: 'text/plain' }) |
||||
} else if (format === 'html') { |
||||
// Parse the AsciiDoc content to HTML
|
||||
const parsedContent = await contentParserService.parseContent(content, { |
||||
eventKind: event.kind, |
||||
enableMath: true, |
||||
enableSyntaxHighlighting: true |
||||
}) |
||||
|
||||
const htmlDocument = `<!DOCTYPE html>
|
||||
<html> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<title>${title}</title> |
||||
<style> |
||||
body { font-family: Georgia, serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; } |
||||
h1 { color: #333; border-bottom: 2px solid #333; padding-bottom: 10px; } |
||||
h2 { color: #555; margin-top: 30px; } |
||||
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: monospace; } |
||||
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; } |
||||
blockquote { border-left: 4px solid #ddd; margin-left: 0; padding-left: 20px; color: #666; } |
||||
</style> |
||||
</head> |
||||
<body> |
||||
<article> |
||||
<h1>${title}</h1> |
||||
${parsedContent.html} |
||||
</article> |
||||
</body> |
||||
</html>` |
||||
|
||||
blob = new Blob([htmlDocument], { type: 'text/html' }) |
||||
} else if (format === 'latex') { |
||||
// Basic LaTeX conversion
|
||||
let processedContent = content.replace(/^= (.+)$/gm, '\\section{$1}') |
||||
processedContent = processedContent.replace(/^== (.+)$/gm, '\\subsection{$1}') |
||||
processedContent = processedContent.replace(/^=== (.+)$/gm, '\\subsubsection{$1}') |
||||
blob = new Blob([processedContent], { type: 'text/plain' }) |
||||
} else if (format === 'pdf' || format === 'epub') { |
||||
// Parse the AsciiDoc content to HTML using the content parser
|
||||
const parsedContent = await contentParserService.parseContent(content, { |
||||
eventKind: event.kind, |
||||
enableMath: true, |
||||
enableSyntaxHighlighting: true |
||||
}) |
||||
|
||||
const htmlDocument = `<!DOCTYPE html>
|
||||
<html> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<title>${title}</title> |
||||
<style> |
||||
body { font-family: Georgia, serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; } |
||||
h1 { color: #333; border-bottom: 2px solid #333; padding-bottom: 10px; } |
||||
h2 { color: #555; margin-top: 30px; } |
||||
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: monospace; } |
||||
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; } |
||||
blockquote { border-left: 4px solid #ddd; margin-left: 0; padding-left: 20px; color: #666; } |
||||
</style> |
||||
</head> |
||||
<body> |
||||
<article> |
||||
<h1>${title}</h1> |
||||
${parsedContent.html} |
||||
</article> |
||||
</body> |
||||
</html>` |
||||
|
||||
blob = new Blob([htmlDocument], { type: 'text/html' }) |
||||
} |
||||
|
||||
const url = URL.createObjectURL(blob) |
||||
const a = document.createElement('a') |
||||
a.href = url |
||||
a.download = filename |
||||
document.body.appendChild(a) |
||||
a.click() |
||||
document.body.removeChild(a) |
||||
URL.revokeObjectURL(url) |
||||
|
||||
logger.info(`[ArticleExportMenu] Exported article as ${format}`) |
||||
} catch (error) { |
||||
logger.error('[ArticleExportMenu] Error exporting article:', error) |
||||
alert('Failed to export article. Please try again.') |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}> |
||||
<Button variant="ghost" size="icon" className="shrink-0"> |
||||
<MoreVertical className="h-4 w-4" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()} className="w-56"> |
||||
<DropdownMenuItem onClick={() => exportArticle('html')}> |
||||
<FileDown className="mr-2 h-4 w-4" /> |
||||
<div> |
||||
<div>Export as HTML</div> |
||||
<div className="text-xs text-muted-foreground">Ready to view in browser</div> |
||||
</div> |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => exportArticle('adoc')}> |
||||
<FileDown className="mr-2 h-4 w-4" /> |
||||
<div> |
||||
<div>Export as AsciiDoc</div> |
||||
<div className="text-xs text-muted-foreground">Raw .adoc file</div> |
||||
</div> |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => exportArticle('pdf')}> |
||||
<FileDown className="mr-2 h-4 w-4" /> |
||||
<div> |
||||
<div>Export as PDF</div> |
||||
<div className="text-xs text-muted-foreground">HTML - use browser Print to PDF</div> |
||||
</div> |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => exportArticle('epub')}> |
||||
<FileDown className="mr-2 h-4 w-4" /> |
||||
<div> |
||||
<div>Export as EPUB</div> |
||||
<div className="text-xs text-muted-foreground">HTML - convert with Calibre</div> |
||||
</div> |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => exportArticle('latex')}> |
||||
<FileDown className="mr-2 h-4 w-4" /> |
||||
<div> |
||||
<div>Export as LaTeX</div> |
||||
<div className="text-xs text-muted-foreground">Basic conversion</div> |
||||
</div> |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
) |
||||
} |
||||
|
||||
Loading…
Reference in new issue