feat: long form article

This commit is contained in:
codytseng
2025-12-18 16:36:31 +08:00
parent 893ae9c072
commit 064c6e7923
2 changed files with 67 additions and 47 deletions

View File

@@ -47,7 +47,7 @@ export default function HighlightButton({ onHighlight, containerRef }: Highlight
// Position button above the selection area, centered horizontally // Position button above the selection area, centered horizontally
setPosition({ setPosition({
top: rect.top + scrollTop - 36, // 36px above the selection top: rect.top + scrollTop - 48, // 48px above the selection
left: rect.left + scrollLeft + rect.width / 2 // Center of the selection left: rect.left + scrollLeft + rect.width / 2 // Center of the selection
}) })
setSelectedText(text) setSelectedText(text)

View File

@@ -1,10 +1,12 @@
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager' import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
import ImageWithLightbox from '@/components/ImageWithLightbox' import ImageWithLightbox from '@/components/ImageWithLightbox'
import HighlightButton from '@/components/HighlightButton'
import PostEditor from '@/components/PostEditor'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNote, toNoteList, toProfile } from '@/lib/link' import { toNote, toNoteList, toProfile } from '@/lib/link'
import { ExternalLink } from 'lucide-react' import { ExternalLink } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo, useRef, useState } from 'react'
import Markdown from 'react-markdown' import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import NostrNode from './NostrNode' import NostrNode from './NostrNode'
@@ -20,6 +22,14 @@ export default function LongFormArticle({
}) { }) {
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
const contentRef = useRef<HTMLDivElement>(null)
const [showHighlightEditor, setShowHighlightEditor] = useState(false)
const [selectedText, setSelectedText] = useState('')
const handleHighlight = (text: string) => {
setSelectedText(text)
setShowHighlightEditor(true)
}
const components = useMemo( const components = useMemo(
() => () =>
@@ -74,54 +84,64 @@ export default function LongFormArticle({
/> />
) )
}) as Components, }) as Components,
[] [event.pubkey]
) )
return ( return (
<div <>
className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`} <div
> ref={contentRef}
<h1 className="break-words">{metadata.title}</h1> className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}
{metadata.summary && (
<blockquote>
<p className="break-words">{metadata.summary}</p>
</blockquote>
)}
{metadata.image && (
<ImageWithLightbox
image={{ url: metadata.image, pubkey: event.pubkey }}
className="w-full aspect-[3/1] object-cover my-0"
/>
)}
<Markdown
remarkPlugins={[remarkGfm, remarkNostr]}
urlTransform={(url) => {
if (url.startsWith('nostr:')) {
return url.slice(6) // Remove 'nostr:' prefix for rendering
}
return url
}}
components={components}
> >
{event.content} <h1 className="break-words">{metadata.title}</h1>
</Markdown> {metadata.summary && (
{metadata.tags.length > 0 && ( <blockquote>
<div className="flex gap-2 flex-wrap pb-2"> <p className="break-words">{metadata.summary}</p>
{metadata.tags.map((tag) => ( </blockquote>
<div )}
key={tag} {metadata.image && (
title={tag} <ImageWithLightbox
className="flex items-center rounded-full px-3 bg-muted text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground" image={{ url: metadata.image, pubkey: event.pubkey }}
onClick={(e) => { className="w-full aspect-[3/1] object-cover my-0"
e.stopPropagation() />
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] })) )}
}} <Markdown
> remarkPlugins={[remarkGfm, remarkNostr]}
#<span className="truncate">{tag}</span> urlTransform={(url) => {
</div> if (url.startsWith('nostr:')) {
))} return url.slice(6) // Remove 'nostr:' prefix for rendering
</div> }
)} return url
</div> }}
components={components}
>
{event.content}
</Markdown>
{metadata.tags.length > 0 && (
<div className="flex gap-2 flex-wrap pb-2">
{metadata.tags.map((tag) => (
<div
key={tag}
title={tag}
className="flex items-center rounded-full px-3 bg-muted text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground"
onClick={(e) => {
e.stopPropagation()
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
}}
>
#<span className="truncate">{tag}</span>
</div>
))}
</div>
)}
</div>
<HighlightButton onHighlight={handleHighlight} containerRef={contentRef} />
<PostEditor
highlightedText={selectedText}
parentStuff={event}
open={showHighlightEditor}
setOpen={setShowHighlightEditor}
/>
</>
) )
} }