Something went wrong. Try again.
FlockOff is a mass-block tool for the Atmosphere flockoff.app
Something went wrong. Try again.
5.7 kB · 161 lines
TSX
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'import { buildProfileUrl } from '@/lib/utils'import type { PostData } from '@/types'
interface PostPreviewProps { post: PostData /** If set and > 0, shows an amber "+N new" badge on the engagement counts. */ newEngagementCount?: number /** Compact mode: lighter card, smaller avatar, 2-line text clamp, no counts or link. */ compact?: boolean}
function formatDate(iso: string): string { try { const d = new Date(iso) if (!iso || isNaN(d.getTime())) return '' return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) } catch { return '' }}
// ── Compact variant ───────────────────────────────────────────────────────────function PostPreviewCompact({ post }: { post: PostData }) { const displayName = post.author.displayName ?? post.author.handle return ( <div className="bg-secondary/40 rounded-lg px-3 py-2.5 flex items-start gap-2.5"> <Avatar className="h-9 w-9 shrink-0 mt-0.5"> <AvatarImage src={post.author.avatar} alt={displayName} /> <AvatarFallback className="text-xs bg-[var(--fo-ember)] text-white"> {displayName.charAt(0).toUpperCase()} </AvatarFallback> </Avatar> <div className="min-w-0"> <p className="text-sm font-semibold leading-tight truncate"> {displayName} <span className="font-normal text-[var(--fo-text3)] font-mono text-xs ml-1.5"> @{post.author.handle} </span> </p> {post.text && ( <p className="text-sm text-[var(--fo-text2)] leading-snug line-clamp-2 mt-0.5"> {post.text} </p> )} </div> </div> )}
// ── Full variant ──────────────────────────────────────────────────────────────function PostPreviewFull({ post, newEngagementCount,}: { post: PostData newEngagementCount?: number}) { const displayName = post.author.displayName ?? post.author.handle const date = formatDate(post.createdAt)
return ( <div className="bg-card border border-border rounded-xl p-4 flex flex-col gap-0" style={{ boxShadow: 'var(--fo-shadow-card)' }} > {/* Header row: avatar + author info + link */} <div className="flex items-start gap-3"> <Avatar className="h-12 w-12 shrink-0"> <AvatarImage src={post.author.avatar} alt={displayName} /> <AvatarFallback className="text-sm bg-[var(--fo-ember)] text-white"> {displayName.charAt(0).toUpperCase()} </AvatarFallback> </Avatar> <div className="min-w-0 flex-1 flex items-start justify-between gap-2"> <div className="min-w-0"> <a href={buildProfileUrl(post.author.handle)} target="_blank" rel="noopener noreferrer" className="text-sm font-semibold hover:underline truncate block" > {displayName} </a> <p className="text-xs font-mono text-[var(--fo-text3)]"> @{post.author.handle}{date ? ` · ${date}` : ''} </p> </div> <a href={post.url} target="_blank" rel="noopener noreferrer" className="text-xs font-mono text-[var(--fo-text3)] hover:text-foreground transition-colors shrink-0 mt-0.5" > View on Bluesky ↗ </a> </div> </div>
{/* Post text */} {post.text && ( <p className="text-sm leading-relaxed text-foreground mt-1 mb-3 pl-[60px]"> {post.text} </p> )}
{/* Engagement counts */} <div className="flex items-center gap-2 flex-wrap pl-[60px]"> {post.likeCount != null && ( <StatPill label={`♥ ${post.likeCount.toLocaleString()}`} color="var(--fo-ember)" /> )} {post.repostCount != null && ( <StatPill label={`↺ ${post.repostCount.toLocaleString()}`} color="var(--fo-mutual)" /> )} {post.replyCount != null && ( <StatPill label={`💬 ${post.replyCount.toLocaleString()}`} color="var(--fo-text3)" /> )} {post.quoteCount != null && ( <StatPill label={`❝ ${post.quoteCount.toLocaleString()}`} color="var(--fo-text3)" /> )} {newEngagementCount != null && newEngagementCount > 0 && ( <span className="text-xs font-mono bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30 rounded-full px-1.5 py-0.5"> +{newEngagementCount.toLocaleString()} new </span> )} </div> </div> )}
function StatPill({ label, color }: { label: string; color: string }) { return ( <span className="text-xs font-mono px-2 py-0.5 rounded-full border" style={{ color, borderColor: `color-mix(in oklab, ${color} 30%, transparent)`, backgroundColor: `color-mix(in oklab, ${color} 8%, transparent)`, }} > {label} </span> )}
export function PostPreview({ post, newEngagementCount, compact }: PostPreviewProps) { if (compact) return <PostPreviewCompact post={post} /> return <PostPreviewFull post={post} newEngagementCount={newEngagementCount} />}