'use client'
import { useState, useEffect } from 'react'
import { useAccount } from 'wagmi'
import type { TrumpPost, BotPerformance, Candle } from '@/types'
import { useDashboardStore } from '@/store/dashboard'
import { usePriceSocket } from '@/lib/useRealtimeData'
import { getPrices, getUserPublic } from '@/lib/api'
import ChartPanel from '@/components/dashboard/ChartPanel'
import PostRow, { SignalPill, SourceIcon, fmtPct, timeAgo, TimeAgo, LocalDateTime } from '@/components/dashboard/PostCards'
interface Props {
initialPosts: TrumpPost[]
initialPerformance?: BotPerformance
}
// ── Inline post detail panel shown in the right rail ──────────────────────────
function PostDetail({ post, onClose }: { post: TrumpPost; onClose: () => void }) {
const impact = post.price_impact
function fmtImpactPct(v: number | null | undefined) {
if (v == null || isNaN(v)) return '—'
const s = Math.abs(v).toFixed(2) + '%'
return v >= 0 ? '+' + s : '-' + s
}
return (
{/* header */}
{/* source + time */}
{/* post text */}
{post.text}
{/* signal + sentiment */}
{post.sentiment}
{/* AI confidence */}
AI confidence
{post.ai_confidence}%
{/* AI reasoning */}
{post.ai_reasoning && (
<>
AI reasoning
{post.ai_reasoning}
>
)}
{/* Price impact grid */}
{impact && (
<>
Price impact · {impact.asset}
{([['m5', '5m'], ['m15', '15m'], ['m1h', '1h']] as const).map(([key, label]) => {
const v = impact[key]
const correct = impact[`correct_${key}` as 'correct_m5' | 'correct_m15' | 'correct_m1h']
return (
{label}
= 0 ? 'up' : 'down'}`} style={{ fontSize: 14, fontWeight: 600 }}>
{fmtImpactPct(v)}
{correct != null && (
{correct ? '✓' : '✗'}
)}
)
})}
>
)}
)
}
// ── Empty state for when nothing is selected ──────────────────────────────────
function SelectHint() {
return (
Click any marker on the chart
or a post below to see details
)
}
// ── Main dashboard ─────────────────────────────────────────────────────────────
export default function DashboardClient({ initialPosts, initialPerformance }: Props) {
const { asset, setAsset, timeframe, setTimeframe: _setTimeframe, setLivePrice, setSubscribed, setBotReadiness, setHlApiKeySet, isSubscribed, hlApiKeySet, botReadiness, livePrices } = useDashboardStore()
function setTimeframe(tf: string) { _setTimeframe(tf as '5m' | '15m' | '1H' | '4H' | '1D' | '1W') }
const { address, isConnected } = useAccount()
const [posts, setPosts] = useState(initialPosts)
const [candles, setCandles] = useState([])
const [selectedPostId, setSelectedPostId] = useState(null)
// For 1D: show all posts from selected day
const [selectedDayPosts, setSelectedDayPosts] = useState(null)
useEffect(() => {
if (!isConnected || !address) {
setSubscribed(false)
setHlApiKeySet(false)
setBotReadiness('unknown')
return
}
// Clear account-scoped bot state immediately when the connected wallet
// changes so a previous wallet's status never leaks into the next one.
setSubscribed(false)
setHlApiKeySet(false)
setBotReadiness('unknown')
getUserPublic(address.toLowerCase())
.then((user) => {
setSubscribed(user.active)
setHlApiKeySet(user.hl_api_key_set)
setBotReadiness(user.hl_api_key_set ? 'saved' : 'unknown')
})
.catch(() => {})
}, [address, isConnected, setSubscribed, setHlApiKeySet, setBotReadiness])
usePriceSocket({
onPrice: (a, price) => setLivePrice(a, price),
onNewPost: (post) => setPosts((prev) => [post as TrumpPost, ...prev].slice(0, 500)),
})
useEffect(() => {
setCandles([])
getPrices(asset, timeframe)
.then(setCandles)
.catch(() => {})
}, [asset, timeframe])
const selectedPost = posts.find(p => p.id === selectedPostId) ?? null
const recentPosts = posts.slice(0, 8)
const lastCandle = candles[candles.length - 1]
// Live price beats the most recent candle close — the candle is only
// refreshed when the user changes asset/timeframe, so without WS the number
// is frozen at page load.
const livePrice = livePrices[asset]
const displayPrice = livePrice ?? lastCandle?.close ?? null
// 24-hour change. Picking by candle index is wrong (200×4H = 33 days).
// Find the candle whose `time` is closest to (now − 24h) and use its open.
const now = lastCandle ? lastCandle.time : Math.floor(Date.now() / 1000)
const target24h = now - 24 * 3600
let baseline24h: typeof lastCandle | undefined
if (candles.length) {
baseline24h = candles[0]
for (const c of candles) {
if (c.time <= target24h) baseline24h = c
else break
}
}
const priceChange =
displayPrice != null && baseline24h
? ((displayPrice - baseline24h.open) / baseline24h.open) * 100
: 0
const totalPosts = posts.length
const todayKey = new Date().toISOString().slice(0, 10)
const signalsToday = posts.filter(p => p.published_at?.slice(0, 10) === todayKey).length
const actionablePosts = posts.filter(p => p.signal === 'buy' || p.signal === 'short').length
const winRate = initialPerformance?.win_rate ?? 0
const netPnl = initialPerformance?.net_pnl_usd ?? 0
const hasPriceData = candles.length > 0
const hasPerformanceData = Boolean(initialPerformance)
return (
Signal monitor
{actionablePosts} actionable signals · Auto-trader {botReadiness === 'ready' ? 'ready' : hlApiKeySet ? 'saved' : isSubscribed ? 'subscribed' : 'standby'} · {hasPriceData ? 'Live market context' : 'Waiting for market data'}
Live feed
{totalPosts} posts tracked
{/* KPI Row */}
BTC
{displayPrice != null ? '$' + Math.round(displayPrice).toLocaleString() : '—'}
= 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'}
24h
Signals today
{signalsToday}
{actionablePosts} actionable total
30d Net P&L
{netPnl >= 0 ? '+$' : '-$'}{Math.abs(netPnl).toLocaleString('en-US', { maximumFractionDigits: 0 })}
{hasPerformanceData ? 'Bot performance' : 'Performance pending'}
Win rate
{initialPerformance ? (winRate * 100).toFixed(1) + '%' : '—'}
{hasPerformanceData ? `${initialPerformance?.total_trades ?? 0} trades` : 'Waiting for trade history'}
{/* Left: Chart + signal stream */}
Price · {asset}
{displayPrice != null ? '$' + Math.round(displayPrice).toLocaleString() : '—'}
= 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} · 24h
{(['5m', '15m', '1H', '4H', '1D'] as const).map(t => (
))}
{
setSelectedDayPosts(null)
setSelectedPostId(id)
}}
onSelectDayPosts={(dayPosts) => {
setSelectedDayPosts(dayPosts)
setSelectedPostId(null)
}}
/>
Buy signal
Short signal
Hold / filtered
Click marker to see details →
{/* Recent signals list */}
Recent signals
Showing {recentPosts.length} of {totalPosts}
{recentPosts.map(p => (
))}
{/* Right rail */}
{/* Day-view: all posts from clicked day */}
{selectedDayPosts ? (
{selectedDayPosts.length} posts · this candle
{selectedDayPosts.map(p => {
const expanded = selectedPostId === p.id
const impact = p.price_impact
function fmtImpactPct(v: number | null | undefined) {
if (v == null || isNaN(v)) return '—'
const s = Math.abs(v).toFixed(2) + '%'
return v >= 0 ? '+' + s : '-' + s
}
return (
{/* collapsed row — always visible */}
setSelectedPostId(expanded ? null : p.id)}
style={{ padding:12, cursor:'pointer',
background: expanded ? 'var(--bg-sunk)' : 'transparent' }}>
{p.text}
{/* expanded detail */}
{expanded && (
{p.ai_reasoning && (
<>
AI reasoning
{p.ai_reasoning}
>
)}
{impact && (
<>
Price impact · {impact.asset}
{(['m5','m15','m1h'] as const).map((key) => {
const label = key === 'm5' ? '5m' : key === 'm15' ? '15m' : '1h'
const v = impact[key]
const correct = impact[`correct_${key}` as 'correct_m5'|'correct_m15'|'correct_m1h']
return (
{label}
=0?'up':'down'}`} style={{ fontSize:13, fontWeight:600 }}>
{fmtImpactPct(v)}
{correct != null && (
{correct ? '✓' : '✗'}
)}
)
})}
>
)}
)}
)
})}
) : selectedPost ? (
setSelectedPostId(null)} />
) : (
)}
)
}