import React, { useState } from 'react' import { Link } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { IndividualBookmark } from '../../types/bookmarks' import { formatDate, renderParsedContent } from '../../utils/bookmarkUtils' import RichContent from '../RichContent' import { classifyUrl } from '../../utils/helpers' import { useImageCache } from '../../hooks/useImageCache' import { getPreviewImage, fetchOgImage } from '../../utils/imagePreview' import { naddrEncode } from 'nostr-tools/nip19' interface CardViewProps { bookmark: IndividualBookmark index: number hasUrls: boolean extractedUrls: string[] onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => void authorNpub: string getAuthorDisplayName: () => string handleReadNow: (e: React.MouseEvent) => void articleImage?: string articleSummary?: string contentTypeIcon: IconDefinition readingProgress?: number } export const CardView: React.FC = ({ bookmark, index, hasUrls, extractedUrls, onSelectUrl, authorNpub, getAuthorDisplayName, handleReadNow, articleImage, articleSummary, contentTypeIcon, readingProgress }) => { const firstUrl = hasUrls ? extractedUrls[0] : null const firstUrlClassificationType = firstUrl ? classifyUrl(firstUrl)?.type : null const instantPreview = firstUrl ? getPreviewImage(firstUrl, firstUrlClassificationType || '') : null const [ogImage, setOgImage] = useState(null) 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 // Calculate progress color (matching BlogPostCard logic) let progressColor = '#6366f1' // Default blue (reading) if (readingProgress && readingProgress >= 0.95) { progressColor = '#10b981' // Green (completed) } else if (readingProgress && readingProgress > 0 && readingProgress <= 0.10) { progressColor = 'var(--color-text)' // Neutral text color (started) } // Determine which image to use (article image, instant preview, or OG image) const previewImage = articleImage || instantPreview || ogImage const cachedImage = useImageCache(previewImage || undefined) // Fetch OG image if we don't have any other image React.useEffect(() => { if (firstUrl && !articleImage && !instantPreview && !ogImage) { fetchOgImage(firstUrl).then(setOgImage) } }, [firstUrl, articleImage, instantPreview, ogImage]) // Add loading state for images const [imageLoading, setImageLoading] = useState(false) const [imageError, setImageError] = useState(false) const triggerOpen = () => handleReadNow({ preventDefault: () => {} } as React.MouseEvent) const handleKeyDown: React.KeyboardEventHandler = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() triggerOpen() } } // Get internal route for the bookmark const getInternalRoute = (): string | null => { if (bookmark.kind === 30023) { // Nostr-native article - use /a/ route const dTag = bookmark.tags.find(t => t[0] === 'd')?.[1] if (dTag) { const naddr = naddrEncode({ kind: bookmark.kind, pubkey: bookmark.pubkey, identifier: dTag }) return `/a/${naddr}` } } else if (bookmark.kind === 1) { // Note - use /e/ route return `/e/${bookmark.id}` } else if (firstUrl) { // External URL - use /r/ route return `/r/${encodeURIComponent(firstUrl)}` } return null } return (
{/* Bookmark type icon in top-left corner */}
{(cachedImage || firstUrl) && (
handleReadNow({ preventDefault: () => {} } as React.MouseEvent)} > {!cachedImage && firstUrl && (
)}
)}
{getInternalRoute() ? ( e.stopPropagation()} > {formatDate(bookmark.created_at ?? bookmark.listUpdatedAt)} ) : ( {formatDate(bookmark.created_at ?? bookmark.listUpdatedAt)} )}
{extractedUrls.length > 0 && (
{(urlsExpanded ? extractedUrls : extractedUrls.slice(0, 1)).map((url, urlIndex) => { return ( ) })} {extractedUrls.length > 1 && ( )}
)} {isArticle && articleSummary ? ( ) : bookmark.parsedContent ? (
{shouldTruncate && bookmark.content ? : renderParsedContent(bookmark.parsedContent)}
) : bookmark.content && ( )} {contentLength > 210 && ( )}
{/* Reading progress indicator as separator - always shown */} {isArticle && (
)}
e.stopPropagation()} > {getAuthorDisplayName()}
{/* CTA removed */}
) }