You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
4.5 KiB
132 lines
4.5 KiB
import { cardEventBodyBlurb } from '@/lib/card-event-body-blurb' |
|
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' |
|
import { toNote, toNoteList } from '@/lib/link' |
|
import { useSecondaryPageOptional } from '@/PageManager' |
|
import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' |
|
import { useScreenSizeOptional } from '@/providers/ScreenSizeProvider' |
|
import { cn } from '@/lib/utils' |
|
import { Event, kinds } from 'nostr-tools' |
|
import { useMemo } from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import ArticleCardCoverImage from './ArticleCardCoverImage' |
|
|
|
/** |
|
* Feed / embed / preview surface for NIP-23 long-form (kind 30023): title, summary, image, tags — no “Show more” body. |
|
* Full article stays on the note page ({@link showFull} on {@link Note}). |
|
*/ |
|
export default function LongFormCard({ |
|
event, |
|
className, |
|
/** When false (e.g. parent-reply preview strip), card is non-interactive like the old one-line preview. */ |
|
interactive = true |
|
}: { |
|
event: Event |
|
className?: string |
|
interactive?: boolean |
|
}) { |
|
const { t } = useTranslation() |
|
const screenSize = useScreenSizeOptional() |
|
const isSmallScreen = screenSize?.isSmallScreen ?? false |
|
const secondaryPage = useSecondaryPageOptional() |
|
const push = secondaryPage?.push ?? ((url: string) => { |
|
window.location.href = url |
|
}) |
|
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey, event) |
|
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) |
|
const bodyBlurb = useMemo(() => cardEventBodyBlurb(event.content), [event.content]) |
|
const summaryText = (metadata.summary?.trim() || bodyBlurb).trim() |
|
|
|
const displayTitle = metadata.title?.trim() || t('Long-form Article') |
|
|
|
const handleCardClick = (e: React.MouseEvent) => { |
|
if (!interactive) return |
|
e.stopPropagation() |
|
push(toNote(event)) |
|
} |
|
|
|
const titleComponent = ( |
|
<div className="min-w-0 text-xl font-semibold break-words sm:line-clamp-2">{displayTitle}</div> |
|
) |
|
|
|
const tagsComponent = interactive && metadata.tags.length > 0 && ( |
|
<div className="flex flex-wrap gap-1"> |
|
{metadata.tags.map((tag) => ( |
|
<div |
|
key={tag} |
|
className="flex max-w-32 cursor-pointer items-center rounded-full bg-muted px-2.5 py-0.5 text-xs text-muted-foreground 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> |
|
) |
|
|
|
const tagsReadonly = !interactive && metadata.tags.length > 0 && ( |
|
<div className="flex flex-wrap gap-1"> |
|
{metadata.tags.map((tag) => ( |
|
<span |
|
key={tag} |
|
className="flex max-w-32 items-center rounded-full bg-muted px-2.5 py-0.5 text-xs text-muted-foreground" |
|
> |
|
#<span className="truncate">{tag}</span> |
|
</span> |
|
))} |
|
</div> |
|
) |
|
|
|
const summaryComponent = summaryText ? ( |
|
<div className="text-base text-muted-foreground line-clamp-4 break-words">{summaryText}</div> |
|
) : null |
|
|
|
const shellClass = cn(className, !interactive && 'pointer-events-none') |
|
const cardClass = cn( |
|
'rounded-lg border p-4 transition-colors', |
|
interactive && 'cursor-pointer hover:bg-muted/50' |
|
) |
|
|
|
if (isSmallScreen) { |
|
return ( |
|
<div className={shellClass}> |
|
<div className={cardClass} onClick={interactive ? handleCardClick : undefined}> |
|
<ArticleCardCoverImage |
|
event={event} |
|
imageUrl={metadata.image} |
|
autoLoadMedia={autoLoadMedia} |
|
layout="stacked" |
|
/> |
|
<div className="space-y-2"> |
|
{titleComponent} |
|
{summaryComponent} |
|
{tagsComponent} |
|
{tagsReadonly} |
|
</div> |
|
</div> |
|
</div> |
|
) |
|
} |
|
|
|
return ( |
|
<div className={cn('w-full min-w-0', shellClass)}> |
|
<div className={cn(cardClass, 'min-w-0')} onClick={interactive ? handleCardClick : undefined}> |
|
<div className="flex min-w-0 gap-4"> |
|
<ArticleCardCoverImage |
|
event={event} |
|
imageUrl={metadata.image} |
|
autoLoadMedia={autoLoadMedia} |
|
layout="row" |
|
/> |
|
<div className="min-w-0 flex-1 basis-0 space-y-2 overflow-hidden"> |
|
{titleComponent} |
|
{summaryComponent} |
|
{tagsComponent} |
|
{tagsReadonly} |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
) |
|
}
|
|
|