feat: revamp dashboard, trades, and add landing/legal pages
- Major UI updates across dashboard, analytics, posts, trades, settings - New landing page, robots/sitemap, contact/privacy/terms pages - Updated globals.css with extensive styling and new landing.css - Refactor signedRequest, realtime data hook, and dashboard store Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,7 @@ import { useDashboardStore } from '@/store/dashboard'
|
||||
import { usePriceSocket } from '@/lib/useRealtimeData'
|
||||
import { getPrices, getUserPublic } from '@/lib/api'
|
||||
import ChartPanel from '@/components/dashboard/ChartPanel'
|
||||
import BotPanel from '@/components/dashboard/BotPanel'
|
||||
import PostRow, { SignalPill, SourceIcon, fmtPct, timeAgo } from '@/components/dashboard/PostCards'
|
||||
import PostRow, { SignalPill, SourceIcon, fmtPct, timeAgo, TimeAgo, LocalDateTime } from '@/components/dashboard/PostCards'
|
||||
|
||||
interface Props {
|
||||
initialPosts: TrumpPost[]
|
||||
@@ -47,7 +46,7 @@ function PostDetail({ post, onClose }: { post: TrumpPost; onClose: () => void })
|
||||
<SourceIcon source={post.source} />
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, fontFamily: 'var(--mono)' }}>@realDonaldTrump</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--ink-3)' }}>{timeAgo(post.published_at)} ago · {new Date(post.published_at).toLocaleString()}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--ink-3)' }}><TimeAgo iso={post.published_at} suffix=" ago" /> · <LocalDateTime iso={post.published_at} /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,9 +62,9 @@ function PostDetail({ post, onClose }: { post: TrumpPost; onClose: () => void })
|
||||
</div>
|
||||
|
||||
{/* AI confidence */}
|
||||
<div className="row between" style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 6 }}>
|
||||
<div className="ai-metric-head">
|
||||
<span>AI confidence</span>
|
||||
<span className="mono" style={{ color: 'var(--ink)', fontWeight: 600 }}>{post.ai_confidence}%</span>
|
||||
<strong>{post.ai_confidence}%</strong>
|
||||
</div>
|
||||
<div className="confidence-bar" style={{ marginBottom: 16 }}>
|
||||
<div style={{ width: post.ai_confidence + '%' }} />
|
||||
@@ -74,19 +73,8 @@ function PostDetail({ post, onClose }: { post: TrumpPost; onClose: () => void })
|
||||
{/* AI reasoning */}
|
||||
{post.ai_reasoning && (
|
||||
<>
|
||||
<div className="tiny" style={{ marginBottom: 8 }}>AI reasoning</div>
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: 'var(--bg-sunk)',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
border: '1px solid var(--line)',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.6,
|
||||
color: 'var(--ink-2)',
|
||||
marginBottom: 16,
|
||||
maxHeight: 120,
|
||||
overflowY: 'auto',
|
||||
}}>
|
||||
<div className="ai-reasoning-label">AI reasoning</div>
|
||||
<div className="ai-reasoning-card scroll" style={{ marginBottom: 16 }}>
|
||||
{post.ai_reasoning}
|
||||
</div>
|
||||
</>
|
||||
@@ -144,26 +132,35 @@ function SelectHint() {
|
||||
|
||||
// ── Main dashboard ─────────────────────────────────────────────────────────────
|
||||
export default function DashboardClient({ initialPosts, initialPerformance }: Props) {
|
||||
const { asset, setAsset, timeframe, setTimeframe: _setTimeframe, setLivePrice, setSubscribed, setHlApiKeySet, isSubscribed } = useDashboardStore()
|
||||
const { asset, setAsset, timeframe, setTimeframe: _setTimeframe, setLivePrice, setSubscribed, setBotReadiness, setHlApiKeySet, isSubscribed, hlApiKeySet, botReadiness } = useDashboardStore()
|
||||
function setTimeframe(tf: string) { _setTimeframe(tf as '5m' | '15m' | '1H' | '4H' | '1D' | '1W') }
|
||||
const { address, isConnected } = useAccount()
|
||||
const [posts, setPosts] = useState<TrumpPost[]>(initialPosts)
|
||||
const [candles, setCandles] = useState<Candle[]>([])
|
||||
const [selectedPostId, setSelectedPostId] = useState<number | null>(null)
|
||||
// For 1D: show all posts from selected day
|
||||
const [selectedDayPosts, setSelectedDayPosts] = useState<TrumpPost[] | null>(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])
|
||||
}, [address, isConnected, setSubscribed, setHlApiKeySet, setBotReadiness])
|
||||
|
||||
usePriceSocket({
|
||||
onPrice: (a, price) => setLivePrice(a, price),
|
||||
@@ -187,9 +184,13 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
|
||||
: 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 (
|
||||
<div className="page wide">
|
||||
@@ -197,7 +198,7 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
|
||||
<div>
|
||||
<h1 className="page-title">Signal monitor</h1>
|
||||
<p className="page-sub">
|
||||
{actionablePosts} actionable signals · Auto-trader {isSubscribed ? 'running' : 'standby'} · Model v4-selective
|
||||
{actionablePosts} actionable signals · Auto-trader {botReadiness === 'ready' ? 'ready' : hlApiKeySet ? 'saved' : isSubscribed ? 'subscribed' : 'standby'} · {hasPriceData ? 'Live market context' : 'Waiting for market data'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="row gap-s">
|
||||
@@ -212,24 +213,24 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
|
||||
<div className="label"><span className="asset-dot btc" style={{ width: 10, height: 10 }} /> BTC</div>
|
||||
<div className="value">{lastCandle ? '$' + Math.round(lastCandle.close).toLocaleString() : '—'}</div>
|
||||
<div className="foot">
|
||||
<span className={`delta ${priceChange >= 0 ? 'up' : 'down'}`}>{fmtPct(priceChange)}</span>
|
||||
<span className={`delta ${priceChange >= 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'}</span>
|
||||
<span>{timeframe}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="kpi">
|
||||
<div className="label">Signals today</div>
|
||||
<div className="value">{actionablePosts}</div>
|
||||
<div className="foot"><span>Actionable posts</span></div>
|
||||
<div className="value">{signalsToday}</div>
|
||||
<div className="foot"><span>{actionablePosts} actionable total</span></div>
|
||||
</div>
|
||||
<div className="kpi accent">
|
||||
<div className="label">30d Net P&L</div>
|
||||
<div className="value">{netPnl >= 0 ? '+$' : '-$'}{Math.abs(netPnl).toLocaleString(undefined, { maximumFractionDigits: 0 })}</div>
|
||||
<div className="foot"><span>Bot performance</span></div>
|
||||
<div className="value">{netPnl >= 0 ? '+$' : '-$'}{Math.abs(netPnl).toLocaleString('en-US', { maximumFractionDigits: 0 })}</div>
|
||||
<div className="foot"><span>{hasPerformanceData ? 'Bot performance' : 'Performance pending'}</span></div>
|
||||
</div>
|
||||
<div className="kpi">
|
||||
<div className="label">Win rate</div>
|
||||
<div className="value">{initialPerformance ? (winRate * 100).toFixed(1) + '%' : '—'}</div>
|
||||
<div className="foot"><span>{initialPerformance?.total_trades ?? 0} trades</span></div>
|
||||
<div className="foot"><span>{hasPerformanceData ? `${initialPerformance?.total_trades ?? 0} trades` : 'Waiting for trade history'}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -244,7 +245,7 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
|
||||
<div className="hero-value mono" style={{ fontSize: 32 }}>
|
||||
{lastCandle ? '$' + Math.round(lastCandle.close).toLocaleString() : '—'}
|
||||
</div>
|
||||
<span className={`chip ${priceChange >= 0 ? 'up' : 'down'}`}>{fmtPct(priceChange)} · {timeframe}</span>
|
||||
<span className={`chip ${priceChange >= 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} · {timeframe}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stack gap-s" style={{ alignItems: 'flex-end' }}>
|
||||
@@ -269,7 +270,14 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
|
||||
posts={posts}
|
||||
candles={candles}
|
||||
externalSelectedId={selectedPostId}
|
||||
onSelectPost={setSelectedPostId}
|
||||
onSelectPost={(id) => {
|
||||
setSelectedDayPosts(null)
|
||||
setSelectedPostId(id)
|
||||
}}
|
||||
onSelectDayPosts={(dayPosts) => {
|
||||
setSelectedDayPosts(dayPosts)
|
||||
setSelectedPostId(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -289,26 +297,120 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
|
||||
</div>
|
||||
<div className="post-stream">
|
||||
{recentPosts.map(p => (
|
||||
<PostRow
|
||||
key={p.id}
|
||||
post={p}
|
||||
selected={selectedPostId === p.id}
|
||||
onClick={() => setSelectedPostId(selectedPostId === p.id ? null : p.id)}
|
||||
/>
|
||||
<PostRow key={p.id} post={p} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right rail: bot stats + post detail */}
|
||||
{/* Right rail */}
|
||||
<div className="rail">
|
||||
<BotPanel performance={initialPerformance} />
|
||||
{/* Day-view: all posts from clicked day */}
|
||||
{selectedDayPosts ? (
|
||||
<div className="card" style={{ padding: 20 }}>
|
||||
<div className="row between" style={{ marginBottom: 14 }}>
|
||||
<div>
|
||||
<div className="tiny">{selectedDayPosts.length} posts · this candle</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 600, marginTop: 2 }}>
|
||||
<LocalDateTime iso={selectedDayPosts[0].published_at} opts={{ month:'short', day:'numeric', hour:'2-digit', minute:'2-digit' }} />
|
||||
</div>
|
||||
</div>
|
||||
<button className="icon-btn" onClick={() => { setSelectedDayPosts(null); setSelectedPostId(null) }}
|
||||
style={{ width:26, height:26, borderRadius:'50%' }}>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display:'flex', flexDirection:'column', gap:10 }}>
|
||||
{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 (
|
||||
<div key={p.id}
|
||||
style={{ border:`1px solid ${expanded?'var(--amber)':'var(--line)'}`,
|
||||
borderRadius:'var(--r-sm)', overflow:'hidden', transition:'border-color 0.15s' }}>
|
||||
{/* collapsed row — always visible */}
|
||||
<div onClick={() => setSelectedPostId(expanded ? null : p.id)}
|
||||
style={{ padding:12, cursor:'pointer',
|
||||
background: expanded ? 'var(--bg-sunk)' : 'transparent' }}>
|
||||
<div className="row between" style={{ marginBottom:6 }}>
|
||||
<div className="row gap-s">
|
||||
<SignalPill signal={p.signal} />
|
||||
<span className={`chip ${p.sentiment === 'bullish' ? 'up' : p.sentiment === 'bearish' ? 'down' : 'neutral'}`}
|
||||
style={{ fontSize:10 }}>{p.sentiment}</span>
|
||||
</div>
|
||||
<span style={{ fontSize:11, color:'var(--ink-3)' }}><TimeAgo iso={p.published_at} /></span>
|
||||
</div>
|
||||
<p style={{ fontSize:12, lineHeight:1.5, margin:'0 0 8px', color:'var(--ink-2)',
|
||||
...(expanded ? {} : {
|
||||
display:'-webkit-box', WebkitLineClamp: 3,
|
||||
WebkitBoxOrient:'vertical', overflow:'hidden'
|
||||
}) }}>
|
||||
{p.text}
|
||||
</p>
|
||||
<div style={{ display:'flex', gap:6, alignItems:'center' }}>
|
||||
<div style={{ flex:1, height:3, background:'var(--line)', borderRadius:2, overflow:'hidden' }}>
|
||||
<div style={{ width:p.ai_confidence+'%', height:'100%', background:'var(--amber)', borderRadius:2 }} />
|
||||
</div>
|
||||
<span style={{ fontSize:10, color:'var(--ink-3)', whiteSpace:'nowrap' }}>{p.ai_confidence}% conf</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Post detail — shown when something is selected, else hint */}
|
||||
{selectedPost
|
||||
? <PostDetail post={selectedPost} onClose={() => setSelectedPostId(null)} />
|
||||
: <SelectHint />
|
||||
}
|
||||
{/* expanded detail */}
|
||||
{expanded && (
|
||||
<div style={{ padding:'0 12px 12px', borderTop:'1px solid var(--line)' }}>
|
||||
{p.ai_reasoning && (
|
||||
<>
|
||||
<div className="ai-reasoning-label" style={{ margin:'10px 0 8px' }}>AI reasoning</div>
|
||||
<div className="ai-reasoning-card" style={{ padding: '14px 15px', fontSize: 14, lineHeight: 1.7 }}>
|
||||
{p.ai_reasoning}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{impact && (
|
||||
<>
|
||||
<div className="tiny" style={{ margin:'10px 0 6px' }}>Price impact · {impact.asset}</div>
|
||||
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:6 }}>
|
||||
{(['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 (
|
||||
<div key={key} style={{ padding:8, background:'var(--bg-sunk)',
|
||||
borderRadius:'var(--r-sm)', border:'1px solid var(--line)', textAlign:'center' }}>
|
||||
<div style={{ fontSize:10, color:'var(--ink-3)', marginBottom:3 }}>{label}</div>
|
||||
<div className={`delta ${(v??0)>=0?'up':'down'}`} style={{ fontSize:13, fontWeight:600 }}>
|
||||
{fmtImpactPct(v)}
|
||||
</div>
|
||||
{correct != null && (
|
||||
<div style={{ fontSize:10, marginTop:3, color: correct ? 'var(--up)' : 'var(--down)' }}>
|
||||
{correct ? '✓' : '✗'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : selectedPost ? (
|
||||
<PostDetail post={selectedPost} onClose={() => setSelectedPostId(null)} />
|
||||
) : (
|
||||
<SelectHint />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user