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.
290 lines
7.7 KiB
290 lines
7.7 KiB
<script lang="ts"> |
|
/** |
|
* @fileoverview AEventPreview Component - Alexandria |
|
* |
|
* A card component for displaying nostr event previews with configurable display options. |
|
* Shows event metadata, content, author information, and action buttons. |
|
* |
|
* @component |
|
* @category Cards |
|
* |
|
* @prop {NDKEvent} event - The nostr event to display (required) |
|
* @prop {string} [label=""] - Optional label/category for the event |
|
* @prop {boolean} [community=false] - Whether this is a community event |
|
* @prop {number} [truncateContentAt=200] - Character limit for content truncation |
|
* @prop {boolean} [showKind=true] - Whether to show event kind |
|
* @prop {boolean} [showSummary=true] - Whether to show event summary |
|
* @prop {boolean} [showDeferralNaddr=true] - Whether to show deferral naddr |
|
* @prop {boolean} [showPublicationLink=true] - Whether to show publication link |
|
* @prop {boolean} [showContent=true] - Whether to show event content |
|
* @prop {Array<{label: string, onClick: (ev: NDKEvent) => void, variant?: string}>} [actions] - Action buttons |
|
* @prop {(ev: NDKEvent) => void} [onSelect] - Callback when event is selected |
|
* @prop {(naddr: string, ev: NDKEvent) => void} [onDeferralClick] - Callback for deferral clicks |
|
* |
|
* @example |
|
* ```svelte |
|
* <AEventPreview |
|
* {event} |
|
* label="Article" |
|
* showContent={true} |
|
* actions={[{label: "View", onClick: handleView}]} |
|
* /> |
|
* ``` |
|
* |
|
* @example Basic event preview |
|
* ```svelte |
|
* <AEventPreview {event} /> |
|
* ``` |
|
* |
|
* @example Community event with actions |
|
* ```svelte |
|
* <AEventPreview |
|
* {event} |
|
* community={true} |
|
* actions={[ |
|
* {label: "Reply", onClick: handleReply}, |
|
* {label: "Share", onClick: handleShare, variant: "light"} |
|
* ]} |
|
* /> |
|
* ``` |
|
* |
|
* @example Minimal preview without content |
|
* ```svelte |
|
* <AEventPreview |
|
* {event} |
|
* showContent={false} |
|
* showSummary={false} |
|
* truncateContentAt={100} |
|
* /> |
|
* ``` |
|
* |
|
* @features |
|
* - Responsive card layout with author badges |
|
* - Content truncation with "show more" functionality |
|
* - Publication links and metadata display |
|
* - Configurable action buttons |
|
* - Community event highlighting |
|
* - Event kind and summary display |
|
* |
|
* @accessibility |
|
* - Semantic card structure |
|
* - Keyboard accessible action buttons |
|
* - Screen reader friendly metadata |
|
* - Proper heading hierarchy |
|
*/ |
|
|
|
import { Card, Button } from "flowbite-svelte"; |
|
import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte"; |
|
import { userBadge } from "$lib/snippets/UserSnippets.svelte"; |
|
import { toNpub, getMatchingTags } from "$lib/utils/nostrUtils"; |
|
import type { NDKEvent } from "$lib/utils/nostrUtils"; |
|
import { preventDefault } from "svelte/legacy"; |
|
|
|
let { |
|
event, |
|
label = "", |
|
community = false, |
|
truncateContentAt = 200, |
|
showKind = true, |
|
showSummary = true, |
|
showDeferralNaddr = true, |
|
showPublicationLink = true, |
|
showContent = true, |
|
actions, |
|
onSelect, |
|
onDeferralClick, |
|
}: { |
|
event: NDKEvent; |
|
label?: string; |
|
community?: boolean; |
|
truncateContentAt?: number; |
|
showKind?: boolean; |
|
showSummary?: boolean; |
|
showDeferralNaddr?: boolean; |
|
showPublicationLink?: boolean; |
|
showContent?: boolean; |
|
actions?: { |
|
label: string; |
|
onClick: (ev: NDKEvent) => void; |
|
variant?: "primary" | "light" | "alternative"; |
|
}[]; |
|
onSelect?: (ev: NDKEvent) => void; |
|
onDeferralClick?: (naddr: string, ev: NDKEvent) => void; |
|
} = $props(); |
|
|
|
type ProfileData = { |
|
name?: string; |
|
display_name?: string; |
|
about?: string; |
|
picture?: string; |
|
banner?: string; |
|
website?: string; |
|
lud16?: string; |
|
nip05?: string; |
|
}; |
|
|
|
function parseProfileContent(ev: NDKEvent): ProfileData | null { |
|
if (ev.kind !== 0 || !ev.content) { |
|
return null; |
|
} |
|
try { |
|
return JSON.parse(ev.content) as ProfileData; |
|
} catch { |
|
return null; |
|
} |
|
} |
|
|
|
function getSummary(ev: NDKEvent): string | undefined { |
|
return getMatchingTags(ev, "summary")[0]?.[1]; |
|
} |
|
|
|
function getDeferralNaddr(ev: NDKEvent): string | undefined { |
|
return getMatchingTags(ev, "deferral")[0]?.[1]; |
|
} |
|
|
|
const profileData = parseProfileContent(event); |
|
const summary = showSummary ? getSummary(event) : undefined; |
|
const deferralNaddr = showDeferralNaddr ? getDeferralNaddr(event) : undefined; |
|
|
|
function clippedContent(content: string): string { |
|
if (!showContent) { |
|
return ""; |
|
} |
|
if (!truncateContentAt || content.length <= truncateContentAt) { |
|
return content; |
|
} |
|
return content.slice(0, truncateContentAt) + "..."; |
|
} |
|
|
|
function handleSelect(): void { |
|
onSelect?.(event); |
|
} |
|
|
|
function handleKeydown(e: KeyboardEvent): void { |
|
if (e.key === "Enter" || e.key === " ") { |
|
e.preventDefault(); |
|
handleSelect(); |
|
} |
|
} |
|
|
|
function handleDeferralClick(e: MouseEvent): void { |
|
e.stopPropagation(); |
|
if (deferralNaddr) { |
|
onDeferralClick?.(deferralNaddr, event); |
|
} |
|
} |
|
|
|
const displayName: string | undefined = |
|
profileData?.display_name || profileData?.name; |
|
const avatarFallback: string = (displayName || event.pubkey || "?") |
|
.slice(0, 1) |
|
.toUpperCase(); |
|
const createdDate: string = event.created_at |
|
? new Date(event.created_at * 1000).toLocaleDateString() |
|
: "Unknown date"; |
|
|
|
const computedActions = |
|
actions && actions.length > 0 |
|
? actions |
|
: [ |
|
{ |
|
label: "Open", |
|
onClick: (ev: NDKEvent) => onSelect?.(ev), |
|
variant: "light" as const, |
|
}, |
|
]; |
|
</script> |
|
|
|
<Card |
|
class="event-preview-card" |
|
role="group" |
|
tabindex="0" |
|
aria-label="Event preview" |
|
onclick={handleSelect} |
|
onkeydown={handleKeydown} |
|
size="xl" |
|
> |
|
<!-- Header --> |
|
<div class="card-header"> |
|
<!-- Meta --> |
|
<div class="flex flex-row w-full gap-3 items-center min-w-0"> |
|
{#if label} |
|
<span class="event-label"> |
|
{label} |
|
</span> |
|
{/if} |
|
{#if showKind} |
|
<span class="event-kind-badge"> |
|
Kind {event.kind} |
|
</span> |
|
{/if} |
|
{#if community} |
|
<span |
|
class="community-badge" |
|
title="Has posted to the community" |
|
> |
|
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"> |
|
<path |
|
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" |
|
/> |
|
</svg> |
|
Community |
|
</span> |
|
{/if} |
|
<span class="text-xs ml-auto mb-4"> |
|
{createdDate} |
|
</span> |
|
</div> |
|
|
|
<div class="flex flex-row"> |
|
{@render userBadge(toNpub(event.pubkey) as string, displayName)} |
|
</div> |
|
</div> |
|
|
|
<!-- Body --> |
|
<div class="card-body"> |
|
{#if event.kind === 0 && profileData?.about} |
|
<div class="card-about"> |
|
{clippedContent(profileData.about)} |
|
</div> |
|
{:else} |
|
{#if summary} |
|
<div class="card-summary"> |
|
{summary} |
|
</div> |
|
{/if} |
|
{#if deferralNaddr} |
|
<div class="text-xs text-primary-800 dark:text-primary-300"> |
|
Read |
|
<span |
|
class="deferral-link" |
|
role="button" |
|
tabindex="0" |
|
onclick={handleDeferralClick} |
|
onkeydown={(e) => { |
|
if (e.key === "Enter" || e.key === " ") { |
|
e.preventDefault(); |
|
handleDeferralClick(e as unknown as MouseEvent); |
|
} |
|
}} |
|
> |
|
{deferralNaddr} |
|
</span> |
|
</div> |
|
{/if} |
|
|
|
{#if showContent && event.content} |
|
<div class="card-content"> |
|
{clippedContent(event.content)} |
|
</div> |
|
{/if} |
|
{/if} |
|
</div> |
|
|
|
<!-- Footer / Actions --> |
|
{#if showPublicationLink && event.kind !== 0} |
|
<div class="card-footer"> |
|
<ViewPublicationLink {event} /> |
|
</div> |
|
{/if} |
|
</Card>
|
|
|