import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify'; import { ExternalLink, GitFork, Package } from 'lucide-react'; import { useMemo } from 'react'; import { Link } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { ExternalFavicon } from '@/components/ExternalFavicon'; import { Skeleton } from '@/components/ui/skeleton'; import { NostrURI } from '@/lib/NostrURI'; import { cn } from '@/lib/utils'; /** Get a tag value by name. */ function getTag(tags: string[][], name: string): string | undefined { return tags.find(([n]) => n === name)?.[1]; } /** Get all values for a tag name. */ function getAllTags(tags: string[][], name: string): string[] { return tags.filter(([n]) => n === name).map(([, v]) => v); } /** Parse kind-0-style metadata from the content field. */ function parseHandlerMetadata(content: string): NostrMetadata { if (!content) return {}; try { return JSON.parse(content) as NostrMetadata; } catch { return {}; } } /** Get the website URL from web handler tags or metadata. */ function getWebsiteUrl(tags: string[][], metadata: NostrMetadata): string | undefined { const webTags = tags.filter(([n]) => n === 'web'); for (const tag of webTags) { const url = tag[1]; if (url) { const base = url.replace(//g, '').replace(/\/+$/, ''); return base; } } return metadata.website; } /** Extract the display domain from a URL. */ function displayDomain(url: string): string { try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return url; } } /** Build a Shakespeare "Edit with Shakespeare" URL from a kind 30617 `a` tag, if present. */ function getShakespeareUrl(tags: string[][]): string | undefined { for (const tag of tags) { if (tag[0] !== 'a') continue; const parts = tag[1]?.split(':'); if (!parts || parts[0] !== '30617' || parts.length < 3) continue; const pubkey = parts[1]; const identifier = parts.slice(2).join(':'); const nostrUri = new NostrURI({ pubkey, identifier }).toString(); return `https://shakespeare.diy/clone?url=${encodeURIComponent(nostrUri)}`; } return undefined; } interface AppHandlerContentProps { event: NostrEvent; /** If true, show compact preview (used in NoteCard feed). */ compact?: boolean; } /** Renders a kind 31990 NIP-89 application handler event as a showcase-style card. */ export function AppHandlerContent({ event, compact }: AppHandlerContentProps) { const metadata = useMemo(() => parseHandlerMetadata(event.content), [event.content]); const name = metadata.name || getTag(event.tags, 'name') || getTag(event.tags, 'd') || 'Unknown App'; const about = metadata.about; const picture = metadata.picture; const banner = metadata.banner; const websiteUrl = getWebsiteUrl(event.tags, metadata); const hashtags = getAllTags(event.tags, 't'); const shakespeareUrl = useMemo(() => getShakespeareUrl(event.tags), [event.tags]); if (compact) { return (
{/* Banner hero */} {banner && (
)} {/* Content */}
{/* App icon — overlaps the banner hero like a profile avatar */}
{picture ? ( {name} { (e.currentTarget as HTMLElement).style.display = 'none'; }} /> ) : (
)}
{/* Name + domain */}

{name}

{websiteUrl && (
{displayDomain(websiteUrl)}
)}
{/* Description */} {about && (

{about}

)} {/* Tags */} {hashtags.length > 0 && (
{hashtags.slice(0, 4).map((tag) => ( e.stopPropagation()} > #{tag} ))}
)} {/* Actions */}
{websiteUrl && ( )} {shakespeareUrl && ( )}
); } // Full detail view return (
{/* Banner hero */} {banner && (
)} {/* Content */}
{/* App icon — overlaps the banner hero like a profile avatar */}
{picture ? ( {name} { (e.currentTarget as HTMLElement).style.display = 'none'; }} /> ) : (
)}
{/* Name + domain */}

{name}

{websiteUrl && (
{displayDomain(websiteUrl)}
)}
{/* Description */} {about && (

{about}

)} {/* Tags */} {hashtags.length > 0 && (
{hashtags.map((tag) => ( e.stopPropagation()} > #{tag} ))}
)} {/* Actions */}
{websiteUrl && ( )} {shakespeareUrl && ( )}
); } /** Skeleton loading state for AppHandlerContent. */ export function AppHandlerSkeleton() { return (
); }