first day of vibe coding

This commit is contained in:
k
2026-04-20 22:11:18 +08:00
commit 1747fc489f
38 changed files with 15267 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
'use client'
import { useTranslations } from 'next-intl'
import { useState } from 'react'
import { useAccount } from 'wagmi'
import { useConnectModal } from '@rainbow-me/rainbowkit'
import type { BotPerformance } from '@/types'
import { useDashboardStore } from '@/store/dashboard'
import { formatPct, formatHold } from '@/lib/utils'
const MOCK_PERFORMANCE: BotPerformance = {
period_days: 30,
total_trades: 89,
win_rate: 0.73,
net_pnl_usd: 12840,
avg_hold_seconds: 14 * 60,
max_drawdown_pct: 8.2,
}
interface BotPanelProps {
performance?: BotPerformance
}
export default function BotPanel({ performance = MOCK_PERFORMANCE }: BotPanelProps) {
const t = useTranslations('bot')
const { isSubscribed, setSubscribed } = useDashboardStore()
const { address, isConnected } = useAccount()
const { openConnectModal } = useConnectModal()
const [apiKey, setApiKey] = useState('')
const stats = [
{ label: t('winRate'), value: formatPct(performance.win_rate * 100 - 100 + performance.win_rate * 100), display: `${Math.round(performance.win_rate * 100)}%` },
{ label: t('netPnl'), value: `+$${performance.net_pnl_usd.toLocaleString()}`, positive: true },
{ label: t('totalTrades'), value: String(performance.total_trades) },
{ label: t('avgHold'), value: formatHold(performance.avg_hold_seconds) },
]
return (
<div className="w-[300px] shrink-0 flex flex-col gap-3">
{/* Performance card */}
<div className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-5">
<p className="text-[11px] uppercase tracking-wider text-[#555555] mb-4">
{t('title')} · {t('period')}
</p>
<div className="grid grid-cols-2 gap-3">
{stats.map((stat) => (
<div key={stat.label}>
<p className="text-[10px] text-[#555555] mb-0.5">{stat.label}</p>
<p className={`text-[16px] font-medium ${stat.positive ? 'text-[#4ade80]' : 'text-white'}`}>
{stat.display ?? stat.value}
</p>
</div>
))}
</div>
</div>
{/* Divider */}
<div className="h-px bg-[#141414]" />
{/* Conditional bottom section */}
{!isConnected && (
<div className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-5 flex flex-col gap-3">
<p className="text-[14px] font-medium text-white">{t('connectCta')}</p>
<p className="text-[12px] text-[#555555] leading-relaxed">{t('connectDesc')}</p>
<button
onClick={openConnectModal}
className="w-full bg-[#f97316] text-black font-medium text-[13px] rounded-lg py-2.5 hover:bg-[#fb923c] transition-colors"
>
{t('connectWalletFree')}
</button>
</div>
)}
{isConnected && !isSubscribed && (
<div className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-5 flex flex-col gap-3">
<div className="flex items-center justify-between">
<p className="text-[13px] font-medium text-[#333333]">{t('settingsTitle')}</p>
<span className="text-[10px] text-[#333333] border border-[#1e1e1e] rounded-full px-2 py-0.5">
Locked
</span>
</div>
{/* Disabled API key input */}
<div className="opacity-40">
<label className="text-[11px] text-[#555555] block mb-1">{t('apiKeyLabel')}</label>
<input
disabled
placeholder={t('apiKeyPlaceholder')}
className="w-full bg-[#060606] border border-[#141414] rounded-lg px-3 py-2 text-[12px] text-[#333333] placeholder-[#222222] cursor-not-allowed"
/>
</div>
<button
onClick={() => setSubscribed(true)}
className="w-full bg-[#f97316] text-black font-medium text-[13px] rounded-lg py-2.5 hover:bg-[#fb923c] transition-colors"
>
{t('subscribeBtn')}
</button>
</div>
)}
{isConnected && isSubscribed && (
<div className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-5 flex flex-col gap-3">
<div className="flex items-center justify-between">
<p className="text-[13px] font-medium text-white">{t('settingsTitle')}</p>
<span className="text-[10px] text-[#4ade80] border border-[#1a4a2a] bg-[#0d2e1a] rounded-full px-2 py-0.5">
{t('activeStatus')}
</span>
</div>
<div>
<label className="text-[11px] text-[#555555] block mb-1">{t('apiKeyLabel')}</label>
<input
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={t('apiKeyPlaceholder')}
className="w-full bg-[#060606] border border-[#141414] rounded-lg px-3 py-2 text-[12px] text-white placeholder-[#333333] focus:outline-none focus:border-[#222222]"
/>
</div>
<button className="w-full bg-[#141414] text-white border border-[#1a1a1a] text-[13px] rounded-lg py-2 hover:bg-[#0d0d0d] transition-colors">
{t('saveKey')}
</button>
</div>
)}
</div>
)
}
+225
View File
@@ -0,0 +1,225 @@
'use client'
import { useEffect, useRef } from 'react'
import type { TrumpPost, Candle } from '@/types'
import { useDashboardStore } from '@/store/dashboard'
import Pill from '@/components/ui/Pill'
import PostCards, { MOCK_POSTS } from './PostCards'
import ExpandDetail from './ExpandDetail'
interface ChartPanelProps {
posts?: TrumpPost[]
candles?: Candle[]
}
export default function ChartPanel({ posts = MOCK_POSTS, candles = [] }: ChartPanelProps) {
const { asset, timeframe, setAsset, setTimeframe, selectedPostId, setSelectedPost } = useDashboardStore()
const containerRef = useRef<HTMLDivElement>(null)
const chartRef = useRef<unknown>(null)
const seriesRef = useRef<unknown>(null)
const fittedRef = useRef(false)
// Keep latest posts/selectedPostId accessible inside chart callbacks without re-subscribing
const postsRef = useRef(posts)
postsRef.current = posts
const selectedPostIdRef = useRef(selectedPostId)
selectedPostIdRef.current = selectedPostId
const assets: Array<'BTC' | 'ETH'> = ['BTC', 'ETH']
const timeframes: Array<'5m' | '15m' | '1H' | '4H' | '1D' | '1W'> = ['5m', '15m', '1H', '4H', '1D', '1W']
const selectedPost = posts.find((p) => p.id === selectedPostId) ?? null
// Create chart once on mount
useEffect(() => {
if (!containerRef.current || typeof window === 'undefined') return
let destroyed = false
import('lightweight-charts').then(({ createChart, CrosshairMode }) => {
if (destroyed || !containerRef.current) return
const chart = createChart(containerRef.current, {
width: containerRef.current.clientWidth,
height: 360,
layout: {
background: { color: '#050505' },
textColor: '#555555',
},
grid: {
vertLines: { color: '#111111' },
horzLines: { color: '#111111' },
},
crosshair: { mode: CrosshairMode.Normal },
rightPriceScale: { borderColor: '#1a1a1a' },
timeScale: {
borderColor: '#1a1a1a',
timeVisible: true,
rightOffset: 5,
barSpacing: 10,
},
handleScroll: true,
handleScale: true,
})
// @ts-expect-error lightweight-charts type
chartRef.current = chart
const series = chart.addCandlestickSeries({
upColor: '#4ade80',
downColor: '#ef4444',
borderUpColor: '#4ade80',
borderDownColor: '#ef4444',
wickUpColor: '#4ade80',
wickDownColor: '#ef4444',
})
// @ts-expect-error lightweight-charts type
seriesRef.current = series
// Click on chart: find nearest post marker within 2-bar tolerance
chart.subscribeClick((param: { time?: number | string }) => {
if (!param.time) return
const clickTime = typeof param.time === 'number' ? param.time : 0
if (!clickTime) return
const allPosts = postsRef.current
let closest: TrumpPost | null = null
let closestDiff = Infinity
for (const p of allPosts) {
if (!p.published_at) continue
const pt = Math.floor(new Date(p.published_at).getTime() / 1000)
const diff = Math.abs(pt - clickTime)
if (diff < closestDiff) {
closestDiff = diff
closest = p
}
}
// Only select if click is within 2 hours of a post (7200 seconds)
if (closest && closestDiff <= 7200) {
const newId = closest.id === selectedPostIdRef.current ? null : closest.id
setSelectedPost(newId)
} else {
setSelectedPost(null)
}
})
const ro = new ResizeObserver(() => {
if (containerRef.current && !destroyed) {
chart.applyOptions({ width: containerRef.current.clientWidth })
}
})
ro.observe(containerRef.current)
return () => {
ro.disconnect()
}
})
return () => {
destroyed = true
fittedRef.current = false
if (chartRef.current) {
// @ts-expect-error lightweight-charts type
chartRef.current.remove()
chartRef.current = null
seriesRef.current = null
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Update candles + markers whenever data changes
useEffect(() => {
const series = seriesRef.current
const chart = chartRef.current
if (!series || !chart || candles.length === 0) return
const sorted = [...candles].sort((a, b) => a.time - b.time)
// @ts-expect-error lightweight-charts type
series.setData(sorted.map((c) => ({
time: c.time as number,
open: c.open,
high: c.high,
low: c.low,
close: c.close,
})))
// Show markers for all posts within visible time range
const minTime = sorted[0].time
const maxTime = sorted[sorted.length - 1].time
const visible = posts.filter((p) => {
if (!p.published_at) return false
const t = Math.floor(new Date(p.published_at).getTime() / 1000)
return t >= minTime && t <= maxTime
})
if (visible.length > 0) {
const markers = [...visible]
.sort((a, b) => new Date(a.published_at).getTime() - new Date(b.published_at).getTime())
.map((p) => ({
time: Math.floor(new Date(p.published_at).getTime() / 1000) as number,
position: 'aboveBar' as const,
color: p.id === selectedPostId
? '#fb923c'
: p.sentiment === 'bearish'
? '#ef4444'
: '#f97316',
shape: 'circle' as const,
text: '',
size: p.id === selectedPostId ? 2 : 1,
}))
// @ts-expect-error lightweight-charts type
series.setMarkers(markers)
}
if (!fittedRef.current) {
// @ts-expect-error lightweight-charts type
chart.timeScale().fitContent()
fittedRef.current = true
}
}, [candles, posts, selectedPostId])
// Reset fit flag on timeframe/asset switch
useEffect(() => {
fittedRef.current = false
}, [asset, timeframe])
return (
<div className="flex-1 min-w-0 bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-4 flex flex-col gap-3">
{/* Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{assets.map((a) => (
<Pill key={a} active={asset === a} onClick={() => setAsset(a)}>
{a}
</Pill>
))}
</div>
<div className="flex items-center gap-2">
{timeframes.map((tf) => (
<Pill key={tf} active={timeframe === tf} onClick={() => setTimeframe(tf as '4H' | '1D' | '1W')}>
{tf}
</Pill>
))}
</div>
</div>
{/* Chart */}
<div
ref={containerRef}
className="w-full rounded-lg overflow-hidden cursor-crosshair"
style={{ height: 360, background: '#050505' }}
/>
{/* Selected post detail — shown immediately below chart */}
<ExpandDetail post={selectedPost} />
{/* Post cards list */}
<PostCards posts={posts} />
</div>
)
}
+87
View File
@@ -0,0 +1,87 @@
'use client'
import type { TrumpPost } from '@/types'
import Badge from '@/components/ui/Badge'
import { formatPct } from '@/lib/utils'
interface ExpandDetailProps {
post: TrumpPost | null
}
export default function ExpandDetail({ post }: ExpandDetailProps) {
return (
<div
className={`overflow-hidden transition-all duration-300 ease-in-out ${
post ? 'max-h-[300px] opacity-100 mt-3' : 'max-h-0 opacity-0'
}`}
>
{post && (
<div className="bg-[#0a0a0a] border border-[#f97316] rounded-[10px] p-5">
<div className="flex items-start gap-4">
{/* Post content */}
<div className="flex-1">
<div className="flex items-center gap-2 mb-3">
<Badge variant={post.source} />
<Badge variant={post.sentiment} />
<span className="text-[11px] text-[#555555]">
{new Date(post.published_at).toLocaleString()}
</span>
</div>
<p className="text-[13px] text-[#e2e8f0] leading-relaxed">{post.text}</p>
</div>
{/* Stats */}
<div className="shrink-0 w-[260px]">
{/* AI Confidence */}
<div className="mb-4">
<div className="flex items-center justify-between mb-1.5">
<span className="text-[11px] uppercase tracking-wider text-[#555555]">
AI Confidence
</span>
<span className="text-[13px] font-medium text-[#818cf8]">
{post.ai_confidence}%
</span>
</div>
<div className="h-1.5 bg-[#141414] rounded-full">
<div
className="h-full bg-[#818cf8] rounded-full transition-all duration-500"
style={{ width: `${post.ai_confidence}%` }}
/>
</div>
</div>
{/* Price impact */}
{post.price_impact ? (
<div>
<p className="text-[11px] uppercase tracking-wider text-[#555555] mb-2">
Price Impact ({post.price_impact.asset})
</p>
<div className="grid grid-cols-3 gap-2">
{[
{ label: '5m', value: post.price_impact.m5 },
{ label: '15m', value: post.price_impact.m15 },
{ label: '1h', value: post.price_impact.m1h },
].map((item) => (
<div key={item.label} className="bg-[#050505] border border-[#141414] rounded-lg p-2 text-center">
<p className="text-[10px] text-[#555555] mb-1">{item.label}</p>
<p
className={`text-[13px] font-medium ${
item.value >= 0 ? 'text-[#4ade80]' : 'text-[#ef4444]'
}`}
>
{formatPct(item.value)}
</p>
</div>
))}
</div>
</div>
) : (
<p className="text-[12px] text-[#333333]">No significant price impact detected.</p>
)}
</div>
</div>
</div>
)}
</div>
)
}
+77
View File
@@ -0,0 +1,77 @@
'use client'
import { useTranslations } from 'next-intl'
import type { BotPerformance } from '@/types'
import { formatPrice, formatPct } from '@/lib/utils'
import { useDashboardStore } from '@/store/dashboard'
interface KpiRowProps {
performance?: BotPerformance
postsCount?: number
}
interface StatCard {
labelKey: string
value: string
change?: string
changePositive?: boolean
}
export default function KpiRow({ performance, postsCount }: KpiRowProps) {
const t = useTranslations('kpi')
const { livePrices } = useDashboardStore()
const stats: StatCard[] = [
{
labelKey: 'btcPrice',
value: formatPrice(livePrices.BTC ?? 94230),
change: livePrices.BTC ? 'live' : '+1.4%',
changePositive: true,
},
{
labelKey: 'ethPrice',
value: formatPrice(livePrices.ETH ?? 1847),
change: livePrices.ETH ? 'live' : '-0.8%',
changePositive: false,
},
{
labelKey: 'postsTracked',
value: postsCount != null ? postsCount.toLocaleString() : '1,247',
change: '+12 today',
changePositive: true,
},
{
labelKey: 'avgMove',
value: performance
? formatPct(performance.net_pnl_usd > 0 ? 2.3 : -2.3)
: formatPct(2.3),
change: 'after Trump posts',
changePositive: true,
},
]
return (
<div className="grid grid-cols-4 gap-4">
{stats.map((stat) => (
<div
key={stat.labelKey}
className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-5"
>
<p className="text-[11px] uppercase tracking-wider text-[#555555] mb-2">
{t(stat.labelKey as 'btcPrice' | 'ethPrice' | 'postsTracked' | 'avgMove')}
</p>
<p className="text-[22px] font-medium text-white leading-none mb-1.5">{stat.value}</p>
{stat.change && (
<p
className={`text-[12px] ${
stat.changePositive ? 'text-[#4ade80]' : 'text-[#ef4444]'
}`}
>
{stat.change}
</p>
)}
</div>
))}
</div>
)
}
+181
View File
@@ -0,0 +1,181 @@
'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 timeAgo(iso: string): 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`
}
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)
// Auto-scroll selected card into view
useEffect(() => {
if (selectedRef.current && scrollRef.current) {
selectedRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' })
}
}, [selectedPostId])
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>
{/* 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>
</div>
)
}
export { MOCK_POSTS }