done
This commit is contained in:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user