This commit is contained in:
k
2026-04-21 19:32:53 +08:00
parent 1747fc489f
commit 83e5892ddf
25 changed files with 2582 additions and 916 deletions
+57 -163
View File
@@ -1,181 +1,75 @@
'use client'
import { useEffect, useRef } from 'react'
import { useTranslations } from 'next-intl'
import type { TrumpPost } from '@/types'
import Badge from '@/components/ui/Badge'
import { useDashboardStore } from '@/store/dashboard'
import { formatPct } from '@/lib/utils'
const MOCK_POSTS: TrumpPost[] = [
{
id: 1,
text: 'BITCOIN IS THE FUTURE OF MONEY! We will make America the crypto capital of the world. BIG things coming very soon. The dollar will be STRONGER than ever with Bitcoin as our reserve!',
source: 'truth',
published_at: new Date(Date.now() - 1000 * 60 * 14).toISOString(),
sentiment: 'bullish',
ai_confidence: 91,
relevant: true,
price_impact: {
asset: 'BTC',
m5: 1.2,
m15: 2.8,
m1h: 3.4,
price_at_post: 92800,
},
},
{
id: 2,
text: 'Ethereum and all these so-called "smart contracts" are nothing but a scam by the radical left globalists. Very bad for America. We need REAL money, not fake computer tricks!',
source: 'x',
published_at: new Date(Date.now() - 1000 * 60 * 47).toISOString(),
sentiment: 'bearish',
ai_confidence: 84,
relevant: true,
price_impact: {
asset: 'ETH',
m5: -1.9,
m15: -3.1,
m1h: -4.2,
price_at_post: 1920,
},
},
{
id: 3,
text: "Met with many great business leaders today. Discussed the future of America's economy. Jobs are coming back FAST. Nobody builds like Trump!",
source: 'truth',
published_at: new Date(Date.now() - 1000 * 60 * 120).toISOString(),
sentiment: 'neutral',
ai_confidence: 42,
relevant: false,
price_impact: null,
},
{
id: 4,
text: 'Crypto regulations will be ELIMINATED under my watch. No more Biden weaponization of the SEC against Bitcoin holders. We will have the most CRYPTO FRIENDLY government in history!',
source: 'truth',
published_at: new Date(Date.now() - 1000 * 60 * 210).toISOString(),
sentiment: 'bullish',
ai_confidence: 88,
relevant: true,
price_impact: {
asset: 'BTC',
m5: 0.9,
m15: 2.1,
m1h: 2.7,
price_at_post: 91500,
},
},
]
interface PostCardsProps {
posts?: TrumpPost[]
function fmtPct(n: number | null | undefined) {
if (n == null || isNaN(n)) return '—'
const s = n.toFixed(2) + '%'
return n >= 0 ? '+' + s : s
}
function timeAgo(iso: string): string {
function timeAgo(iso: string) {
const diff = Date.now() - new Date(iso).getTime()
const mins = Math.floor(diff / 60000)
if (mins < 60) return `${mins}m ago`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours}h ago`
return `${Math.floor(hours / 24)}d ago`
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'
}
export default function PostCards({ posts = MOCK_POSTS }: PostCardsProps) {
const t = useTranslations('common')
const { selectedPostId, setSelectedPost } = useDashboardStore()
const selectedRef = useRef<HTMLDivElement>(null)
const scrollRef = useRef<HTMLDivElement>(null)
function SourceIcon({ source }: { source: string }) {
if (source === 'x') return <div className="src-ico x">𝕏</div>
return <div className="src-ico truth">T</div>
}
// Auto-scroll selected card into view
useEffect(() => {
if (selectedRef.current && scrollRef.current) {
selectedRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' })
}
}, [selectedPostId])
function SignalPill({ signal }: { signal: string | null }) {
if (!signal || signal === 'hold') return <span className="sig hold">HOLD</span>
return <span className={`sig ${signal}`}>{signal.toUpperCase()}</span>
}
interface PostRowProps {
post: TrumpPost
selected?: boolean
onClick?: () => void
}
export default function PostRow({ post, selected, onClick }: PostRowProps) {
const impact = post.price_impact
return (
<div className="mt-2">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] uppercase tracking-wider text-[#555555]">
Posts · {posts.length}
</span>
{selectedPostId && (
<button
onClick={() => setSelectedPost(null)}
className="text-[11px] text-[#f97316] hover:text-[#fb923c] transition-colors"
>
clear selection ×
</button>
)}
<div className={`post-row ${selected ? 'selected' : ''}`} onClick={onClick}>
<SourceIcon source={post.source} />
<div className="post-body">
<div className="meta">
<span className="mono" style={{ color: 'var(--ink-2)', fontWeight: 500 }}>@realDonaldTrump</span>
<span>·</span>
<span>{timeAgo(post.published_at)} ago</span>
<span>·</span>
<span className={`chip ${post.sentiment === 'bullish' ? 'up' : post.sentiment === 'bearish' ? 'down' : 'neutral'}`} style={{ padding: '2px 8px', fontSize: 11 }}>
{post.sentiment}
</span>
</div>
<p className="text">{post.text.slice(0, 180)}{post.text.length > 180 ? '…' : ''}</p>
</div>
{/* Horizontally scrollable row */}
<div
ref={scrollRef}
className="flex gap-3 overflow-x-auto pb-2"
style={{ scrollbarWidth: 'none' }}
>
{posts.map((post) => {
const isSelected = selectedPostId === post.id
const impact = post.price_impact
return (
<div
key={post.id}
ref={isSelected ? selectedRef : null}
onClick={() => setSelectedPost(isSelected ? null : post.id)}
className={`shrink-0 w-[200px] bg-[#0a0a0a] border rounded-[10px] p-3 cursor-pointer transition-colors hover:bg-[#0d0d0d] ${
isSelected ? 'border-[#f97316]' : 'border-[#141414]'
}`}
>
{/* Header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1">
<Badge variant={post.source} />
<Badge variant={post.sentiment} />
</div>
<span className="text-[10px] text-[#555555]">{timeAgo(post.published_at)}</span>
</div>
{/* Text */}
<p className="text-[11px] text-[#e2e8f0] leading-relaxed mb-2 line-clamp-3">
{post.text.slice(0, 90)}
{post.text.length > 90 ? '…' : ''}
</p>
{/* Confidence bar */}
<div className="mb-2">
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[#555555]">AI</span>
<span className="text-[10px] text-[#818cf8]">{post.ai_confidence}%</span>
</div>
<div className="h-[2px] bg-[#141414] rounded-full">
<div
className="h-full bg-[#818cf8] rounded-full"
style={{ width: `${post.ai_confidence}%` }}
/>
</div>
</div>
{/* Price impact */}
{impact ? (
<span
className={`text-[11px] font-medium ${
impact.m1h >= 0 ? 'text-[#4ade80]' : 'text-[#ef4444]'
}`}
>
{formatPct(impact.m1h)} 1h
</span>
) : (
<span className="text-[10px] text-[#333333]">{t('neutral')}</span>
)}
</div>
)
})}
<div className="post-aside">
<SignalPill signal={post.signal} />
<div className="impact-mini">
{impact ? (
<>
<span className="tf">1h</span>
<span className={`delta ${(impact.m1h ?? 0) >= 0 ? 'up' : 'down'}`}>{fmtPct(impact.m1h)}</span>
</>
) : (
<span className="tf">no data</span>
)}
</div>
<div className="row gap-s" style={{ fontSize: 11, color: 'var(--ink-3)' }}>
<span>AI</span>
<span className="mono" style={{ color: 'var(--ink-2)', fontWeight: 500 }}>{post.ai_confidence}%</span>
</div>
</div>
</div>
)
}
export { MOCK_POSTS }
export { SignalPill, SourceIcon, fmtPct, timeAgo }