'use client' import { useEffect, useState } from 'react' import type { TrumpPost } from '@/types' function fmtPct(n: number | null | undefined) { if (n == null || isNaN(n)) return '—' // null = window not yet closed const s = n.toFixed(2) + '%' return n >= 0 ? '+' + s : s } function fmtImpactPct(v: number | null | undefined) { if (v == null || isNaN(v)) return '—' const s = Math.abs(v).toFixed(2) + '%' return v >= 0 ? '+' + s : '-' + s } function timeAgo(iso: string) { const diff = Date.now() - new Date(iso).getTime() const m = Math.floor(diff / 60000) if (m < 1) return 'just now' if (m < 60) return m + 'm' const h = Math.floor(m / 60) if (h < 24) return h + 'h' return Math.floor(h / 24) + 'd' } /** * Hydration-safe wrapper for relative time. Returns an empty placeholder on * SSR / first client render, then the real relative time after mount. Prevents * the SSR/CSR "5m vs 6m" mismatch error. */ function TimeAgo({ iso, suffix = '' }: { iso: string; suffix?: string }) { const [mounted, setMounted] = useState(false) useEffect(() => { setMounted(true) }, []) if (!mounted) return return {timeAgo(iso)}{suffix} } /** * Hydration-safe absolute date formatter. Uses fixed 'en-US' locale + options * so the server/client outputs match once mounted; renders a placeholder before * mount to avoid timezone mismatches. */ function LocalDateTime({ iso, opts }: { iso: string; opts?: Intl.DateTimeFormatOptions }) { const [mounted, setMounted] = useState(false) useEffect(() => { setMounted(true) }, []) if (!mounted) return return {new Date(iso).toLocaleString('en-US', opts)} } function SourceIcon({ source: _source }: { source: string }) { // Truth Social only — no X/Twitter support. return
T
} function SignalPill({ signal }: { signal: string | null }) { if (!signal || signal === 'hold') return HOLD return {signal.toUpperCase()} } interface PostRowProps { post: TrumpPost selected?: boolean onClick?: () => void } export default function PostRow({ post, selected, onClick }: PostRowProps) { const [expanded, setExpanded] = useState(false) const impact = post.price_impact function handleClick() { if (!onClick) setExpanded(e => !e) onClick?.() } return (
{/* ── main row ── */}
@realDonaldTrump · · {post.sentiment}

{expanded ? post.text : (post.text.slice(0, 180) + (post.text.length > 180 ? '…' : ''))}

{impact ? ( <> 1h peak = 0 ? 'up' : 'down'}`}> {impact.m1h == null ? '…' : fmtPct(impact.m1h)} ) : ( no data )}
AI {post.ai_confidence}%
{/* ── expanded detail ── */} {expanded && (
e.stopPropagation()} > {/* AI confidence bar */}
AI confidence {post.ai_confidence}%
{/* AI reasoning */} {post.ai_reasoning && (
AI reasoning
{post.ai_reasoning}
)} {/* Price impact — peak move in signal direction per window */} {impact && (
Peak move · {impact.asset}
{(['m5', 'm15', 'm1h'] as const).map(key => { const label = key === 'm5' ? '5m' : key === 'm15' ? '15m' : '1h' const v = impact[key] // null = window still open const correct = impact[`correct_${key}` as 'correct_m5' | 'correct_m15' | 'correct_m1h'] const pending = v == null return (
{label}
= 0 ? 'up' : 'down'}`} style={{ fontSize: 14, fontWeight: 600, color: pending ? 'var(--ink-3)' : undefined }} > {pending ? '…' : fmtImpactPct(v)}
{!pending && correct != null && (
{correct ? '✓' : '✗'}
)} {pending && (
live
)}
) })}
)}
)}
) } export { SignalPill, SourceIcon, fmtPct, timeAgo, TimeAgo, LocalDateTime }