diff --git a/src/components/ArticleExportMenu/ArticleExportMenu.tsx b/src/components/ArticleExportMenu/ArticleExportMenu.tsx new file mode 100644 index 0000000..78e38ca --- /dev/null +++ b/src/components/ArticleExportMenu/ArticleExportMenu.tsx @@ -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 = ` + + + + ${title} + + + +
+

${title}

+ ${parsedContent.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 = ` + + + + ${title} + + + +
+

${title}

+ ${parsedContent.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 ( + + e.stopPropagation()}> + + + e.stopPropagation()} className="w-56"> + exportArticle('html')}> + +
+
Export as HTML
+
Ready to view in browser
+
+
+ exportArticle('adoc')}> + +
+
Export as AsciiDoc
+
Raw .adoc file
+
+
+ exportArticle('pdf')}> + +
+
Export as PDF
+
HTML - use browser Print to PDF
+
+
+ exportArticle('epub')}> + +
+
Export as EPUB
+
HTML - convert with Calibre
+
+
+ exportArticle('latex')}> + +
+
Export as LaTeX
+
Basic conversion
+
+
+
+
+ ) +} + diff --git a/src/components/KindFilter/index.tsx b/src/components/KindFilter/index.tsx index 4003c3e..67eb0e1 100644 --- a/src/components/KindFilter/index.tsx +++ b/src/components/KindFilter/index.tsx @@ -16,6 +16,8 @@ const KIND_FILTER_OPTIONS = [ { kindGroup: [kinds.ShortTextNote, ExtendedKind.COMMENT], label: 'Posts' }, { kindGroup: [kinds.Repost], label: 'Reposts' }, { kindGroup: [kinds.LongFormArticle], label: 'Articles' }, + { kindGroup: [ExtendedKind.PUBLICATION], label: 'Publications' }, + { kindGroup: [ExtendedKind.WIKI_ARTICLE], label: 'Wiki Articles' }, { kindGroup: [kinds.Highlights], label: 'Highlights' }, { kindGroup: [ExtendedKind.POLL], label: 'Polls' }, { kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' }, diff --git a/src/components/Note/AsciidocArticle/AsciidocArticle.tsx b/src/components/Note/AsciidocArticle/AsciidocArticle.tsx index e585f0d..60df5b9 100644 --- a/src/components/Note/AsciidocArticle/AsciidocArticle.tsx +++ b/src/components/Note/AsciidocArticle/AsciidocArticle.tsx @@ -16,10 +16,12 @@ import { ExtendedKind } from '@/constants' export default function AsciidocArticle({ event, - className + className, + hideImagesAndInfo = false }: { event: Event className?: string + hideImagesAndInfo?: boolean }) { const { push } = useSecondaryPage() const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) @@ -309,7 +311,7 @@ export default function AsciidocArticle({ /> {/* Image Carousel - Collapsible */} - {allImages.length > 0 && ( + {!hideImagesAndInfo && allImages.length > 0 && ( + + + exportPublication('html')}> + + Export as HTML + + exportPublication('adoc')}> + + Export as AsciiDoc + + exportPublication('pdf')}> + + Export as PDF + + exportPublication('epub')}> + + Export as EPUB + + exportPublication('latex')}> + + Export as LaTeX + + + + {metadata.summary && (

{metadata.summary}

@@ -300,7 +470,7 @@ export default function PublicationIndex({ // Render 30041 or 30818 content as AsciidocArticle return (
- +
) } else { diff --git a/src/components/Note/WikiCard.tsx b/src/components/Note/WikiCard.tsx index 31db1fb..2c17c26 100644 --- a/src/components/Note/WikiCard.tsx +++ b/src/components/Note/WikiCard.tsx @@ -8,6 +8,7 @@ import { nip19 } from 'nostr-tools' import { useMemo } from 'react' import { BookOpen, Globe } from 'lucide-react' import Image from '../Image' +import ArticleExportMenu from '../ArticleExportMenu/ArticleExportMenu' export default function WikiCard({ event, @@ -87,9 +88,10 @@ export default function WikiCard({ const summaryComponent = metadata.summary && (
{metadata.summary}
) - + const buttons = ( -
+
+ {dTag && (