Browse Source

all images to the left

allow landscape images to be twice as wide
imwald
Silberengel 3 months ago
parent
commit
3cd4cf9a8f
  1. 198
      src/components/WebPreview/index.tsx

198
src/components/WebPreview/index.tsx

@ -334,7 +334,7 @@ export default function WebPreview({ url, className }: { url: string; className?
} as Event } as Event
}, [fetchedEvent]) }, [fetchedEvent])
// Determine which image to use for dimension detection (for event cards) // Determine which image to use for event cards
const eventMetadata = fetchedEvent ? getLongFormArticleMetadataFromEvent(fetchedEvent) : null const eventMetadata = fetchedEvent ? getLongFormArticleMetadataFromEvent(fetchedEvent) : null
const eventImage = eventMetadata?.image const eventImage = eventMetadata?.image
const imetaInfos = fetchedEvent ? getImetaInfosFromEvent(fetchedEvent) : [] const imetaInfos = fetchedEvent ? getImetaInfosFromEvent(fetchedEvent) : []
@ -346,51 +346,41 @@ export default function WebPreview({ url, className }: { url: string; className?
} }
const displayImageForDetection = eventImageThumbnail || image const displayImageForDetection = eventImageThumbnail || image
// Detect image aspect ratio to determine layout - MUST be called unconditionally // Detect image aspect ratio to determine width - MUST be called unconditionally
const [imageAspectRatio, setImageAspectRatio] = useState<number | null>(null) const [imageAspectRatio, setImageAspectRatio] = useState<number | null>(null)
const [isImageLoading, setIsImageLoading] = useState(true)
const [ogImageAspectRatio, setOgImageAspectRatio] = useState<number | null>(null) const [ogImageAspectRatio, setOgImageAspectRatio] = useState<number | null>(null)
const [isOgImageLoading, setIsOgImageLoading] = useState(true)
useEffect(() => { useEffect(() => {
if (!displayImageForDetection) { if (!displayImageForDetection) {
setImageAspectRatio(null) setImageAspectRatio(null)
setIsImageLoading(false)
return return
} }
setIsImageLoading(true)
const img = new window.Image() const img = new window.Image()
img.onload = () => { img.onload = () => {
const aspectRatio = img.width / img.height const aspectRatio = img.width / img.height
setImageAspectRatio(aspectRatio) setImageAspectRatio(aspectRatio)
setIsImageLoading(false)
} }
img.onerror = () => { img.onerror = () => {
setImageAspectRatio(null) setImageAspectRatio(null)
setIsImageLoading(false)
} }
img.src = displayImageForDetection img.src = displayImageForDetection
}, [displayImageForDetection]) }, [displayImageForDetection])
// Detect OG image aspect ratio for OG cards // Detect OG image aspect ratio
useEffect(() => { useEffect(() => {
if (!image) { if (!image) {
setOgImageAspectRatio(null) setOgImageAspectRatio(null)
setIsOgImageLoading(false)
return return
} }
setIsOgImageLoading(true)
const img = new window.Image() const img = new window.Image()
img.onload = () => { img.onload = () => {
const aspectRatio = img.width / img.height const aspectRatio = img.width / img.height
setOgImageAspectRatio(aspectRatio) setOgImageAspectRatio(aspectRatio)
setIsOgImageLoading(false)
} }
img.onerror = () => { img.onerror = () => {
setOgImageAspectRatio(null) setOgImageAspectRatio(null)
setIsOgImageLoading(false)
} }
img.src = image img.src = image
}, [image]) }, [image])
@ -419,10 +409,6 @@ export default function WebPreview({ url, className }: { url: string; className?
// The OG image is already converted to absolute URL by useFetchWebMetadata // The OG image is already converted to absolute URL by useFetchWebMetadata
const displayImage = eventImageThumbnail || image 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 // Extract bookstr metadata if applicable
const bookMetadata = fetchedEvent ? extractBookMetadata(fetchedEvent) : null const bookMetadata = fetchedEvent ? extractBookMetadata(fetchedEvent) : null
const isBookstrEvent = fetchedEvent && (fetchedEvent.kind === ExtendedKind.PUBLICATION || fetchedEvent.kind === ExtendedKind.PUBLICATION_CONTENT) && !!bookMetadata?.book const isBookstrEvent = fetchedEvent && (fetchedEvent.kind === ExtendedKind.PUBLICATION || fetchedEvent.kind === ExtendedKind.PUBLICATION_CONTENT) && !!bookMetadata?.book
@ -442,111 +428,16 @@ export default function WebPreview({ url, className }: { url: string; className?
const isMarkdownEvent = fetchedEvent && (fetchedEvent.kind === kinds.LongFormArticle || fetchedEvent.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN) const isMarkdownEvent = fetchedEvent && (fetchedEvent.kind === kinds.LongFormArticle || fetchedEvent.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN)
const showContentPreview = previewEvent && previewEvent.content && (isAsciidocEvent || isMarkdownEvent) const showContentPreview = previewEvent && previewEvent.content && (isAsciidocEvent || isMarkdownEvent)
// Render landscape image on top, portrait on left // Render all images on left side, crop wider ones
if (isLandscape && displayImage) {
return (
<div
className={cn('p-3 flex flex-col 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)}
>
<div className="w-full h-52 -mx-3 -mt-3 mb-3 flex items-center justify-center overflow-hidden bg-gradient-to-r from-green-50/50 to-transparent dark:from-green-950/20">
<Image
image={{ url: displayImage, pubkey: fetchedEvent?.pubkey }}
className="w-full h-full object-contain"
hideIfError
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 mb-1">
{fetchedEvent ? (
<>
<Username userId={fetchedEvent.pubkey} className="text-xs" />
{eventAuthorProfile?.avatar && (
<img
src={eventAuthorProfile.avatar}
alt=""
className="w-5 h-5 rounded-full flex-shrink-0 object-cover"
onError={(e) => {
e.currentTarget.style.display = 'none'
}}
/>
)}
<span className="text-xs text-muted-foreground"></span>
<span className="text-xs text-muted-foreground">{eventTypeName}</span>
</>
) : (
<span className="text-xs text-muted-foreground">
{isFetchingEventFinal ? 'Loading event...' : 'Event'}
</span>
)}
<a
href={cleanedUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="ml-auto"
>
<ExternalLink className="w-3 h-3 text-green-600 dark:text-green-400 flex-shrink-0" />
</a>
</div>
{fetchedEvent && (
<>
{/* Always show title in card header, hide it in content preview */}
{eventTitle && (
<div className="font-semibold text-sm line-clamp-2 mb-1 text-green-900 dark:text-green-100">{eventTitle}</div>
)}
{isBookstrEvent && bookMetadata && (
<div className="text-xs text-muted-foreground space-x-2 mb-1">
{bookMetadata.type && <span>Type: {bookMetadata.type}</span>}
{bookMetadata.book && <span>Book: {formatBookName(bookMetadata.book)}</span>}
{bookMetadata.chapter && <span>Chapter: {bookMetadata.chapter}</span>}
{bookMetadata.verse && <span>Verse: {bookMetadata.verse}</span>}
{bookMetadata.version && <span>Version: {bookMetadata.version.toUpperCase()}</span>}
</div>
)}
{eventSummary && !showContentPreview && (
<div className="text-xs text-muted-foreground line-clamp-2 mb-1">{eventSummary}</div>
)}
{showContentPreview && (
<div className="my-2 text-sm line-clamp-6 overflow-hidden [&_img]:hidden [&_h1]:hidden [&_h2]:hidden">
{isAsciidocEvent ? (
<AsciidocArticle
event={previewEvent}
className="pointer-events-none"
hideImagesAndInfo={true}
/>
) : (
<MarkdownArticle
event={previewEvent}
className="pointer-events-none"
hideMetadata={true}
/>
)}
</div>
)}
</>
)}
<hr className="mt-4 mb-2 border-t border-border" />
<a
href={cleanedUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-xs text-muted-foreground truncate block hover:underline"
>
{truncatedUrl}
</a>
</div>
</div>
)
}
// Render portrait image on left (30% bigger: w-40 * 1.3 = w-52)
return ( return (
<div <div
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)} 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)}
> >
{displayImage && (isPortrait || isImageLoading) && ( {displayImage && (
<div className="w-52 flex-shrink-0 bg-gradient-to-r from-green-50/50 to-transparent dark:from-green-950/20 -my-3 -ml-3 -mr-0 flex items-center justify-center rounded-l-lg overflow-hidden"> <div className={cn(
"flex-shrink-0 bg-gradient-to-r from-green-50/50 to-transparent dark:from-green-950/20 -my-3 -ml-3 -mr-0 flex items-center justify-center rounded-l-lg overflow-hidden",
imageAspectRatio !== null && imageAspectRatio > 1 ? "w-[416px]" : "w-52"
)}>
<Image <Image
image={{ url: displayImage, pubkey: fetchedEvent?.pubkey }} image={{ url: displayImage, pubkey: fetchedEvent?.pubkey }}
className="w-full h-full object-cover" className="w-full h-full object-cover"
@ -734,15 +625,16 @@ export default function WebPreview({ url, className }: { url: string; className?
) )
} }
// Determine OG image orientation // All OG images render on left with cropping
const isOgPortrait = ogImageAspectRatio !== null && ogImageAspectRatio < 1
const isOgLandscape = ogImageAspectRatio !== null && ogImageAspectRatio > 1
if (isSmallScreen && image) { if (isSmallScreen && image) {
// Small screen: always use horizontal layout with image on left // Small screen: always use horizontal layout with image on left
return ( return (
<div className="rounded-lg border mt-2 overflow-hidden flex"> <div className="rounded-lg border mt-2 overflow-hidden flex">
<div className="w-40 flex-shrink-0 bg-muted flex items-center justify-center rounded-l-lg overflow-hidden"> <div className={cn(
"flex-shrink-0 bg-muted flex items-center justify-center rounded-l-lg overflow-hidden",
ogImageAspectRatio !== null && ogImageAspectRatio > 1 ? "w-[320px]" : "w-40"
)}>
<Image image={{ url: image }} className="w-full h-full object-cover" hideIfError /> <Image image={{ url: image }} className="w-full h-full object-cover" hideIfError />
</div> </div>
<div className="bg-muted p-2 w-full flex-1"> <div className="bg-muted p-2 w-full flex-1">
@ -774,55 +666,14 @@ export default function WebPreview({ url, className }: { url: string; className?
) )
} }
// Render OG card with portrait/landscape layout // Render all OG images on left side, crop wider ones
if (isOgLandscape && image) {
return (
<div className={cn('p-2 flex flex-col w-full border rounded-lg overflow-hidden gap-0', className)}>
<div className="w-full h-52 -mx-2 -mt-2 mb-2 flex items-center justify-center overflow-hidden bg-muted">
<Image
image={{ url: image }}
className="w-full h-full object-contain"
hideIfError
/>
</div>
<div className="flex-1 w-0 p-2">
<div className="flex items-center gap-2 mb-1">
<div className="text-xs text-muted-foreground truncate">{hostname}</div>
<a
href={cleanedUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<ExternalLink className="w-3 h-3 text-muted-foreground flex-shrink-0" />
</a>
</div>
{title && <div className="font-semibold line-clamp-2 mb-1">{title}</div>}
{description && (
<div className={cn("line-clamp-3 mb-1", title ? "text-xs text-muted-foreground" : "text-sm font-semibold")}>
{description}
</div>
)}
<hr className="mt-4 mb-2 border-t border-border" />
<a
href={cleanedUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-xs text-muted-foreground truncate block hover:underline"
>
{url}
</a>
</div>
</div>
)
}
// Portrait or square image: render on left
return ( return (
<div className={cn('p-2 flex w-full border rounded-lg overflow-hidden gap-0', className)}> <div className={cn('p-2 flex w-full border rounded-lg overflow-hidden gap-0', className)}>
{image && (isOgPortrait || isOgImageLoading) && ( {image && (
<div className="w-52 flex-shrink-0 bg-muted flex items-center justify-center -my-2 -ml-2 -mr-0 rounded-l-lg overflow-hidden"> <div className={cn(
"flex-shrink-0 bg-muted flex items-center justify-center -my-2 -ml-2 -mr-0 rounded-l-lg overflow-hidden",
ogImageAspectRatio !== null && ogImageAspectRatio > 1 ? "w-[416px]" : "w-52"
)}>
<Image <Image
image={{ url: image }} image={{ url: image }}
className="w-full h-full object-cover" className="w-full h-full object-cover"
@ -830,7 +681,7 @@ export default function WebPreview({ url, className }: { url: string; className?
/> />
</div> </div>
)} )}
<div className="flex-1 w-0 p-2 pl-2"> <div className="flex-1 min-w-0 p-2 pl-2">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<div className="text-xs text-muted-foreground truncate">{hostname}</div> <div className="text-xs text-muted-foreground truncate">{hostname}</div>
<a <a
@ -842,12 +693,15 @@ export default function WebPreview({ url, className }: { url: string; className?
<ExternalLink className="w-3 h-3 text-muted-foreground flex-shrink-0" /> <ExternalLink className="w-3 h-3 text-muted-foreground flex-shrink-0" />
</a> </a>
</div> </div>
{title && <div className="font-semibold line-clamp-2 mb-1">{title}</div>} {title && <div className="font-semibold line-clamp-2 mb-1 break-words">{title}</div>}
{description && ( {description && (
<div className={cn("line-clamp-3 mb-1", title ? "text-xs text-muted-foreground" : "text-sm font-semibold")}> <div className={cn("line-clamp-3 mb-1 break-words", title ? "text-xs text-muted-foreground" : "text-sm font-semibold")}>
{description} {description}
</div> </div>
)} )}
{!title && !description && (
<div className="text-xs text-muted-foreground mb-1">No description available</div>
)}
<hr className="mt-4 mb-2 border-t border-border" /> <hr className="mt-4 mb-2 border-t border-border" />
<a <a
href={cleanedUrl} href={cleanedUrl}

Loading…
Cancel
Save