diff --git a/.cursor/rules/nostr-native-blog-post-aka-long-form-kind.mdc b/.cursor/rules/nostr-native-blog-post-aka-long-form-kind.mdc index 3dca9096..17bce3a3 100644 --- a/.cursor/rules/nostr-native-blog-post-aka-long-form-kind.mdc +++ b/.cursor/rules/nostr-native-blog-post-aka-long-form-kind.mdc @@ -1,3 +1,11 @@ --- -alwaysApply: true +description: anything that has to do with kind:30023 aka nostr blog posts aka nostr-native long-form content +alwaysApply: false --- + +Always stick to NIPs. Do everything with applesauce (getArticleTitle, getArticleSummary, getHashtags, getMentions). + +- https://github.com/hzrd149/applesauce/blob/17c9dbb0f2c263e2ebd01729ea2fa138eca12bd1/packages/docs/tutorial/02-helpers.md +- https://github.com/nostr-protocol/nips/blob/master/19.md +- https://github.com/nostr-protocol/nips/blob/master/23.md +- https://nostrbook.dev/kinds/30023 diff --git a/dist/index.html b/dist/index.html index ca1d04c8..acf972a7 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5,8 +5,8 @@ Boris - Nostr Bookmarks - - + +
diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index 28d46640..375bec84 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -30,13 +30,17 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS const firstUrl = hasUrls ? extractedUrls[0] : null const firstUrlClassification = firstUrl ? classifyUrl(firstUrl) : null + // For kind:30023 articles, get the image from tags + const isArticle = bookmark.kind === 30023 + const articleImage = isArticle ? bookmark.tags.find(t => t[0] === 'image')?.[1] : undefined + // Fetch OG image for large view (hook must be at top level) const instantPreview = firstUrl ? getPreviewImage(firstUrl, firstUrlClassification?.type || '') : null React.useEffect(() => { - if (viewMode === 'large' && firstUrl && !instantPreview && !ogImage) { + if (viewMode === 'large' && firstUrl && !instantPreview && !ogImage && !articleImage) { fetchOgImage(firstUrl).then(setOgImage) } - }, [viewMode, firstUrl, instantPreview, ogImage]) + }, [viewMode, firstUrl, instantPreview, ogImage, articleImage]) // Resolve author profile using applesauce const authorProfile = useEventModel(Models.ProfileModel, [bookmark.pubkey]) @@ -99,7 +103,8 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS authorNpub, eventNevent, getAuthorDisplayName, - handleReadNow + handleReadNow, + articleImage } if (viewMode === 'compact') { @@ -107,9 +112,9 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS } if (viewMode === 'large') { - const previewImage = instantPreview || ogImage + const previewImage = articleImage || instantPreview || ogImage return } - return + return } diff --git a/src/components/BookmarkViews/CardView.tsx b/src/components/BookmarkViews/CardView.tsx index 72344426..7e007117 100644 --- a/src/components/BookmarkViews/CardView.tsx +++ b/src/components/BookmarkViews/CardView.tsx @@ -20,6 +20,7 @@ interface CardViewProps { eventNevent?: string getAuthorDisplayName: () => string handleReadNow: (e: React.MouseEvent) => void + articleImage?: string } export const CardView: React.FC = ({ @@ -33,15 +34,24 @@ export const CardView: React.FC = ({ authorNpub, eventNevent, getAuthorDisplayName, - handleReadNow + handleReadNow, + articleImage }) => { const [expanded, setExpanded] = useState(false) const [urlsExpanded, setUrlsExpanded] = useState(false) const contentLength = (bookmark.content || '').length const shouldTruncate = !expanded && contentLength > 210 + const isArticle = bookmark.kind === 30023 return (
+ {isArticle && articleImage && ( +
handleReadNow({ preventDefault: () => {} } as React.MouseEvent)} + /> + )}
{bookmark.isPrivate ? ( diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index 760054b5..83961ab4 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -14,6 +14,7 @@ interface CompactViewProps { onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => void getIconForUrlType: IconGetter firstUrlClassification: { buttonText: string } | null + articleImage?: string } export const CompactView: React.FC = ({ diff --git a/src/components/BookmarkViews/LargeView.tsx b/src/components/BookmarkViews/LargeView.tsx index ea33c49f..14e32a4f 100644 --- a/src/components/BookmarkViews/LargeView.tsx +++ b/src/components/BookmarkViews/LargeView.tsx @@ -38,13 +38,19 @@ export const LargeView: React.FC = ({ return (
- {hasUrls && ( + {(hasUrls || (isArticle && previewImage)) && (
onSelectUrl?.(extractedUrls[0])} + onClick={() => { + if (isArticle) { + handleReadNow({ preventDefault: () => {} } as React.MouseEvent) + } else { + onSelectUrl?.(extractedUrls[0]) + } + }} style={previewImage ? { backgroundImage: `url(${previewImage})` } : undefined} > - {!previewImage && ( + {!previewImage && hasUrls && (
diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index f51bccdc..18a00c4c 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -140,6 +140,7 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { setReaderContent({ title: article.title, markdown: article.markdown, + image: article.image, url: `nostr:${naddr}` }) } else { @@ -190,6 +191,7 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { title={readerContent?.title} html={readerContent?.html} markdown={readerContent?.markdown} + image={readerContent?.image} selectedUrl={selectedUrl} highlights={highlights} showUnderlines={showUnderlines} diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index 8f16c59e..40ba3602 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -15,6 +15,7 @@ interface ContentPanelProps { html?: string markdown?: string selectedUrl?: string + image?: string highlights?: Highlight[] showUnderlines?: boolean highlightStyle?: 'marker' | 'underline' @@ -29,6 +30,7 @@ const ContentPanel: React.FC = ({ html, markdown, selectedUrl, + image, highlights = [], showUnderlines = true, highlightStyle = 'marker', @@ -158,6 +160,11 @@ const ContentPanel: React.FC = ({ return (
+ {image && ( +
+ {title +
+ )} {title && (

{title}

diff --git a/src/index.css b/src/index.css index 1a7251c6..2c14cc01 100644 --- a/src/index.css +++ b/src/index.css @@ -1023,6 +1023,48 @@ body { background: #218838; } +/* Article hero image in card view */ +.article-hero-image { + width: 100%; + height: 200px; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + cursor: pointer; + transition: all 0.2s ease; + border-radius: 8px 8px 0 0; + position: relative; +} + +.article-hero-image:hover { + opacity: 0.9; +} + +.article-hero-image::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(to bottom, transparent 60%, rgba(0,0,0,0.4) 100%); + pointer-events: none; + border-radius: 8px 8px 0 0; +} + +/* Hero image in reader view */ +.reader-hero-image { + width: 100%; + margin: 0 0 2rem 0; + border-radius: 8px; + overflow: hidden; +} + +.reader-hero-image img { + width: 100%; + height: auto; + max-height: 500px; + object-fit: cover; + display: block; +} + /* Private Bookmark Styles */ .private-bookmark { background: #2a2a2a; diff --git a/src/services/readerService.ts b/src/services/readerService.ts index 9289cce0..b97f5815 100644 --- a/src/services/readerService.ts +++ b/src/services/readerService.ts @@ -6,6 +6,7 @@ export interface ReadableContent { title?: string html?: string markdown?: string + image?: string } interface CachedContent {