diff --git a/src/components/WebPreview/index.tsx b/src/components/WebPreview/index.tsx index bfa19d4..deb9537 100644 --- a/src/components/WebPreview/index.tsx +++ b/src/components/WebPreview/index.tsx @@ -19,6 +19,7 @@ import { Event } from 'nostr-tools' import { BIG_RELAY_URLS } from '@/constants' import { getImetaInfosFromEvent } from '@/lib/event' import MarkdownArticle from '../Note/MarkdownArticle/MarkdownArticle' +import AsciidocArticle from '../Note/AsciidocArticle/AsciidocArticle' // Helper function to get event type name function getEventTypeName(kind: number): string { @@ -333,6 +334,43 @@ export default function WebPreview({ url, className }: { url: string; className? } as Event }, [fetchedEvent]) + // Determine which image to use for dimension detection (for event cards) + const eventMetadata = fetchedEvent ? getLongFormArticleMetadataFromEvent(fetchedEvent) : null + const eventImage = eventMetadata?.image + const imetaInfos = fetchedEvent ? getImetaInfosFromEvent(fetchedEvent) : [] + let eventImageThumbnail: string | null = null + if (eventImage && fetchedEvent) { + const cleanedEventImage = cleanUrl(eventImage) + const matchingImeta = imetaInfos.find(info => cleanUrl(info.url) === cleanedEventImage) + eventImageThumbnail = matchingImeta?.thumb || eventImage + } + const displayImageForDetection = eventImageThumbnail || image + + // Detect image aspect ratio to determine layout - MUST be called unconditionally + const [imageAspectRatio, setImageAspectRatio] = useState(null) + const [isImageLoading, setIsImageLoading] = useState(true) + + useEffect(() => { + if (!displayImageForDetection) { + setImageAspectRatio(null) + setIsImageLoading(false) + return + } + + setIsImageLoading(true) + const img = new window.Image() + img.onload = () => { + const aspectRatio = img.width / img.height + setImageAspectRatio(aspectRatio) + setIsImageLoading(false) + } + img.onerror = () => { + setImageAspectRatio(null) + setIsImageLoading(false) + } + img.src = displayImageForDetection + }, [displayImageForDetection]) + // Early return after ALL hooks are called if (!autoLoadMedia) { return null @@ -349,23 +387,17 @@ export default function WebPreview({ url, className }: { url: string; className? if (!hasOpengraphData || nostrIdentifier) { // Enhanced card for event URLs (always show if nostr identifier detected, even while loading) if (nostrType === 'naddr' || nostrType === 'nevent' || nostrType === 'note') { - const eventMetadata = fetchedEvent ? getLongFormArticleMetadataFromEvent(fetchedEvent) : null const eventTypeName = fetchedEvent ? getEventTypeName(fetchedEvent.kind) : null const eventTitle = eventMetadata?.title || eventTypeName const eventSummary = eventMetadata?.summary || description - const eventImage = eventMetadata?.image - // Extract imeta info to check for thumbnails - const imetaInfos = fetchedEvent ? getImetaInfosFromEvent(fetchedEvent) : [] - // Find thumbnail for the event image if available - let eventImageThumbnail: string | null = null - if (eventImage && fetchedEvent) { - const cleanedEventImage = cleanUrl(eventImage) - // Find imeta info that matches the event image URL - const matchingImeta = imetaInfos.find(info => cleanUrl(info.url) === cleanedEventImage) - // Return thumbnail if available, otherwise return original image - eventImageThumbnail = matchingImeta?.thumb || eventImage - } + // Fallback to OG image from website if event doesn't have an image + // The OG image is already converted to absolute URL by useFetchWebMetadata + const displayImage = eventImageThumbnail || image + + // Determine if image is portrait (taller than wide) or landscape (wider than tall) + const isPortrait = imageAspectRatio !== null && imageAspectRatio < 1 + const isLandscape = imageAspectRatio !== null && imageAspectRatio > 1 // Extract bookstr metadata if applicable const bookMetadata = fetchedEvent ? extractBookMetadata(fetchedEvent) : null @@ -381,22 +413,124 @@ export default function WebPreview({ url, className }: { url: string; className? // Truncate original URL to 150 characters const truncatedUrl = url.length > 150 ? url.substring(0, 150) + '...' : url + // Determine which article component to use based on event kind + const isAsciidocEvent = fetchedEvent && (fetchedEvent.kind === ExtendedKind.WIKI_ARTICLE || fetchedEvent.kind === ExtendedKind.PUBLICATION_CONTENT) + const isMarkdownEvent = fetchedEvent && (fetchedEvent.kind === kinds.LongFormArticle || fetchedEvent.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN) + const showContentPreview = previewEvent && previewEvent.content && (isAsciidocEvent || isMarkdownEvent) + + // Render landscape image on top, portrait on left + if (isLandscape && displayImage) { + return ( +
+
+ +
+
+
+ {fetchedEvent ? ( + <> + + {eventAuthorProfile?.avatar && ( + { + e.currentTarget.style.display = 'none' + }} + /> + )} + + {eventTypeName} + + ) : ( + + {isFetchingEventFinal ? 'Loading event...' : 'Event'} + + )} + e.stopPropagation()} + className="ml-auto" + > + + +
+ {fetchedEvent && ( + <> + {/* Always show title in card header, hide it in content preview */} + {eventTitle && ( +
{eventTitle}
+ )} + {isBookstrEvent && bookMetadata && ( +
+ {bookMetadata.type && Type: {bookMetadata.type}} + {bookMetadata.book && Book: {formatBookName(bookMetadata.book)}} + {bookMetadata.chapter && Chapter: {bookMetadata.chapter}} + {bookMetadata.verse && Verse: {bookMetadata.verse}} + {bookMetadata.version && Version: {bookMetadata.version.toUpperCase()}} +
+ )} + {eventSummary && !showContentPreview && ( +
{eventSummary}
+ )} + {showContentPreview && ( +
+ {isAsciidocEvent ? ( + + ) : ( + + )} +
+ )} + + )} +
+ e.stopPropagation()} + className="text-xs text-muted-foreground truncate block hover:underline" + > + {truncatedUrl} + +
+
+ ) + } + + // Render portrait image on left (30% bigger: w-40 * 1.3 = w-52) return (
{ - e.stopPropagation() - window.open(cleanedUrl, '_blank') - }} + className={cn('p-3 flex w-full border rounded-lg overflow-hidden gap-0 bg-gradient-to-r from-green-50/50 to-transparent dark:from-green-950/20', className)} > - {eventImageThumbnail && fetchedEvent && ( - + {displayImage && (isPortrait || isImageLoading) && ( +
+ +
)} -
+
{fetchedEvent ? ( <> @@ -419,10 +553,19 @@ export default function WebPreview({ url, className }: { url: string; className? {isFetchingEventFinal ? 'Loading event...' : 'Event'} )} - + e.stopPropagation()} + className="ml-auto" + > + +
{fetchedEvent && ( <> + {/* Always show title in card header, hide it in content preview */} {eventTitle && (
{eventTitle}
)} @@ -435,21 +578,38 @@ export default function WebPreview({ url, className }: { url: string; className? {bookMetadata.version && Version: {bookMetadata.version.toUpperCase()}}
)} - {eventSummary && ( + {eventSummary && !showContentPreview && (
{eventSummary}
)} - {previewEvent && previewEvent.content && ( -
- + {showContentPreview && ( +
+ {isAsciidocEvent ? ( + + ) : ( + + )}
)} )} -
{truncatedUrl}
+
+ e.stopPropagation()} + className="text-xs text-muted-foreground truncate block hover:underline" + > + {truncatedUrl} +
) @@ -462,20 +622,18 @@ export default function WebPreview({ url, className }: { url: string; className? return (
{ - e.stopPropagation() - window.open(cleanedUrl, '_blank') - }} + className={cn('p-3 flex w-full border rounded-lg overflow-hidden gap-0 bg-gradient-to-r from-green-50/50 to-transparent dark:from-green-950/20', className)} > {fetchedProfile?.avatar && ( - +
+ +
)} -
+
{fetchedProfile ? ( <> @@ -492,12 +650,29 @@ export default function WebPreview({ url, className }: { url: string; className? {isFetchingProfile ? 'Loading profile...' : 'Profile'} )} - + e.stopPropagation()} + className="ml-auto" + > + +
{fetchedProfile?.about && (
{fetchedProfile.about}
)} -
{truncatedUrl}
+
+ e.stopPropagation()} + className="text-xs text-muted-foreground truncate block hover:underline" + > + {truncatedUrl} +
) @@ -506,18 +681,30 @@ export default function WebPreview({ url, className }: { url: string; className? // Basic fallback for non-nostr URLs - show site information return (
{ - e.stopPropagation() - window.open(cleanedUrl, '_blank') - }} + className={cn('p-3 flex w-full border rounded-lg overflow-hidden gap-3 bg-gradient-to-r from-green-50/50 to-transparent dark:from-green-950/20', className)} >
-
{cleanedUrl}
+
+ e.stopPropagation()} + className="text-xs text-muted-foreground break-all line-clamp-2 block hover:underline" + > + {cleanedUrl} +
) @@ -525,46 +712,61 @@ export default function WebPreview({ url, className }: { url: string; className? if (isSmallScreen && image) { return ( -
{ - e.stopPropagation() - window.open(cleanedUrl, '_blank') - }} - > - -
+
+
+ +
+
) } return ( -
{ - e.stopPropagation() - window.open(cleanedUrl, '_blank') - }} - > +
{image && ( - +
+ +
)} -
+
{title &&
{title}
} {description && ( @@ -572,7 +774,16 @@ export default function WebPreview({ url, className }: { url: string; className? {description}
)} -
{url}
+
+ e.stopPropagation()} + className="text-xs text-muted-foreground truncate block hover:underline" + > + {url} +
)