import { type ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import { Heart, MessageCircle, Repeat2 } from 'lucide-react'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { formatNumber } from '@/lib/formatNumber'; import { cn } from '@/lib/utils'; /** A single image attachment. */ export interface ExternalImage { thumb: string; alt: string; } /** An external link card. */ export interface ExternalExternal { title: string; thumb?: string; } /** * Shared shape for a social post from any external platform * (Bluesky, Mastodon, etc.). */ export interface ExternalPostData { /** Display name of the post author. */ displayName: string; /** Handle / username (without leading @). */ handle: string; /** Avatar URL. */ avatar?: string; /** Plaintext content of the post. */ text: string; /** ISO-8601 creation date. */ createdAt: string; /** Canonical URL of the post on the source platform. */ postUrl: string; /** Canonical URL of the author's profile on the source platform. */ profileUrl: string; /** Reply count. */ replyCount: number; /** Repost / boost count (includes quotes if applicable). */ repostCount: number; /** Like / favourite count. */ likeCount: number; /** Image attachments. */ images?: ExternalImage[]; /** External link card (when there are no images). */ external?: ExternalExternal; /** Small branding icon rendered in the bottom-right. */ brandIcon?: ReactNode; } /** Format an ISO date string into a relative or short date. */ function formatDate(iso: string): string { const date = new Date(iso); const now = Date.now(); const diffSec = Math.floor((now - date.getTime()) / 1000); if (diffSec < 60) return `${diffSec}s`; if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m`; if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h`; if (diffSec < 604800) return `${Math.floor(diffSec / 86400)}d`; if (diffSec < 2592000) return `${Math.floor(diffSec / 604800)}w`; return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } interface ExternalPostCardProps { post: ExternalPostData; /** When true, hides image and external link thumbnails. */ hideImage?: boolean; className?: string; } /** * Renders an external social post as a native quote-post card. * * Clicking the card body navigates to `/i/{postUrl}`. * Clicking the avatar or display name navigates to `/i/{profileUrl}`. */ export function ExternalPostCard({ post, hideImage, className }: ExternalPostCardProps) { const navigate = useNavigate(); return (
{ e.stopPropagation(); navigate(`/i/${encodeURIComponent(post.postUrl)}`); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); navigate(`/i/${encodeURIComponent(post.postUrl)}`); } }} > {/* Images */} {!hideImage && post.images && post.images.length > 0 && (
1 ? 'grid grid-cols-2 gap-px' : '', )}> {post.images.slice(0, 4).map((img, i) => ( {img.alt { (e.currentTarget as HTMLElement).style.display = 'none'; }} /> ))}
)} {/* External link card (if no images) */} {!hideImage && !post.images && post.external?.thumb && (
{ (e.currentTarget.parentElement as HTMLElement).style.display = 'none'; }} />
)} {/* Post content */}
{/* Author row */}
@{post.handle} · {formatDate(post.createdAt)}
{/* Text content */} {post.text && (

{post.text}

)} {/* External link title */} {post.external && post.external.title && (
{post.external.title}
)} {/* Interaction stats */}
{formatNumber(post.replyCount)} {formatNumber(post.repostCount)} {formatNumber(post.likeCount)} {/* Platform branding — links to the original post */} {post.brandIcon && ( e.stopPropagation()} > {post.brandIcon} )}
); } export function ExternalPostCardSkeleton({ className }: { className?: string }) { return (
); }