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
+31
View File
@@ -0,0 +1,31 @@
# 依赖包
/node_modules
/.pnp
.pnp.js
# Next.js 构建输出
/.next/
/out
/build
# 环境变量 (最重要:防止泄露)
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# 调试日志
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# 系统与编辑器文件
.DS_Store
*.pem
.vscode/
.idea/
# 类型定义
*.tsbuildinfo
+65
View File
@@ -0,0 +1,65 @@
'use client'
import { useState, useEffect } from 'react'
import type { TrumpPost, BotTrade, BotPerformance, Candle } from '@/types'
import { useDashboardStore } from '@/store/dashboard'
import { usePriceSocket } from '@/lib/useRealtimeData'
import { getPrices } from '@/lib/api'
import KpiRow from '@/components/dashboard/KpiRow'
import ChartPanel from '@/components/dashboard/ChartPanel'
import BotPanel from '@/components/dashboard/BotPanel'
import TradesTable from '@/components/trades/TradesTable'
interface Props {
initialPosts: TrumpPost[]
initialPerformance?: BotPerformance
initialTrades: BotTrade[]
}
export default function DashboardClient({ initialPosts, initialPerformance, initialTrades }: Props) {
const { asset, timeframe, setLivePrice } = useDashboardStore()
const [posts, setPosts] = useState<TrumpPost[]>(initialPosts)
const [candles, setCandles] = useState<Candle[]>([])
const [candlesLoading, setCandlesLoading] = useState(false)
usePriceSocket({
onPrice: (a, price) => setLivePrice(a, price),
onNewPost: (post) => setPosts((prev) => [post as TrumpPost, ...prev].slice(0, 50)),
})
useEffect(() => {
setCandles([])
setCandlesLoading(true)
getPrices(asset, timeframe)
.then(setCandles)
.catch(() => {})
.finally(() => setCandlesLoading(false))
}, [asset, timeframe])
return (
<div className="pt-14">
<div className="max-w-[1400px] mx-auto px-6 py-6 flex flex-col gap-4">
{/* KPI 行 */}
<KpiRow performance={initialPerformance} postsCount={posts.length} />
{/* 主内容区:图表 + Bot 面板 */}
<div className="flex gap-4 items-start w-full">
<div className="flex-1 min-w-0 relative">
<ChartPanel posts={posts} candles={candles} />
{candlesLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-black/40 rounded-[10px] pointer-events-none">
<span className="text-[#555555] text-sm">Loading...</span>
</div>
)}
</div>
<div className="w-[280px] shrink-0">
<BotPanel performance={initialPerformance} />
</div>
</div>
{/* 交易记录 */}
<TradesTable trades={initialTrades} />
</div>
</div>
)
}
+33
View File
@@ -0,0 +1,33 @@
'use client'
import { WagmiProvider } from 'wagmi'
import { RainbowKitProvider, darkTheme } from '@rainbow-me/rainbowkit'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
import { config } from '@/lib/wagmi'
import '@rainbow-me/rainbowkit/styles.css'
interface ProvidersProps {
children: React.ReactNode
}
export default function Providers({ children }: ProvidersProps) {
const [queryClient] = useState(() => new QueryClient())
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
theme={darkTheme({
accentColor: '#f97316',
accentColorForeground: '#000000',
borderRadius: 'medium',
overlayBlur: 'small',
})}
>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
)
}
+14
View File
@@ -0,0 +1,14 @@
import { getTranslations } from 'next-intl/server'
export default async function AnalyticsPage() {
const t = await getTranslations('analytics')
return (
<div className="max-w-[1400px] mx-auto px-6 py-6">
<div className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-10 text-center">
<h1 className="text-[22px] font-medium text-white mb-3">{t('title')}</h1>
<p className="text-[14px] text-[#555555]">{t('comingSoon')}</p>
</div>
</div>
)
}
+22
View File
@@ -0,0 +1,22 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
box-sizing: border-box;
}
body {
background-color: #000000;
}
/* Remove default focus ring and apply custom */
button:focus-visible {
outline: 1px solid #f97316;
outline-offset: 2px;
}
input:focus-visible {
outline: 1px solid #222222;
outline-offset: 0;
}
+42
View File
@@ -0,0 +1,42 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
import { notFound } from 'next/navigation'
import { locales } from '@/i18n'
import Navbar from '@/components/nav/Navbar'
import Providers from './Providers'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'TrumpSignal — AI-powered crypto trading signals',
description: 'Trade crypto based on Trump social media signals with AI confidence scoring.',
}
interface LayoutProps {
children: React.ReactNode
params: { locale: string }
}
export default async function LocaleLayout({ children, params: { locale } }: LayoutProps) {
if (!locales.includes(locale as (typeof locales)[number])) {
notFound()
}
const messages = await getMessages()
return (
<html lang={locale}>
<body className={`${inter.className} bg-[#000000] min-h-screen text-white`}>
<NextIntlClientProvider messages={messages}>
<Providers>
<Navbar locale={locale} />
<main className="pt-14">{children}</main>
</Providers>
</NextIntlClientProvider>
</body>
</html>
)
}
+20
View File
@@ -0,0 +1,20 @@
import { getPosts, getPerformance, getTrades } from '@/lib/api'
import DashboardClient from './DashboardClient'
export const revalidate = 30
export default async function OverviewPage() {
const [posts, performance, trades] = await Promise.allSettled([
getPosts(500, 1),
getPerformance(),
getTrades(20, 1),
])
return (
<DashboardClient
initialPosts={posts.status === 'fulfilled' ? posts.value : []}
initialPerformance={performance.status === 'fulfilled' ? performance.value : undefined}
initialTrades={trades.status === 'fulfilled' ? trades.value : []}
/>
)
}
+15
View File
@@ -0,0 +1,15 @@
import { useTranslations } from 'next-intl'
import { getTranslations } from 'next-intl/server'
export default async function PostsPage() {
const t = await getTranslations('posts')
return (
<div className="max-w-[1400px] mx-auto px-6 py-6">
<div className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-10 text-center">
<h1 className="text-[22px] font-medium text-white mb-3">{t('title')}</h1>
<p className="text-[14px] text-[#555555]">{t('comingSoon')}</p>
</div>
</div>
)
}
+31
View File
@@ -0,0 +1,31 @@
'use client'
import { useTranslations } from 'next-intl'
import { useAccount } from 'wagmi'
import { useConnectModal } from '@rainbow-me/rainbowkit'
export default function SettingsClient() {
const t = useTranslations('settings')
const { isConnected } = useAccount()
const { openConnectModal } = useConnectModal()
if (!isConnected) {
return (
<div className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-10 text-center">
<p className="text-[14px] text-[#555555] mb-4">{t('connectRequired')}</p>
<button
onClick={openConnectModal}
className="bg-[#f97316] text-black font-medium text-[13px] rounded-lg px-6 py-2.5 hover:bg-[#fb923c] transition-colors"
>
Connect wallet
</button>
</div>
)
}
return (
<div className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-10 text-center">
<p className="text-[14px] text-[#555555]">{t('comingSoon')}</p>
</div>
)
}
+13
View File
@@ -0,0 +1,13 @@
import { getTranslations } from 'next-intl/server'
import SettingsClient from './SettingsClient'
export default async function SettingsPage() {
const t = await getTranslations('settings')
return (
<div className="max-w-[1400px] mx-auto px-6 py-6">
<h1 className="text-[22px] font-medium text-white mb-5">{t('title')}</h1>
<SettingsClient />
</div>
)
}
+17
View File
@@ -0,0 +1,17 @@
import { getTranslations } from 'next-intl/server'
import TradesTable from '@/components/trades/TradesTable'
import { getTrades } from '@/lib/api'
export const revalidate = 10
export default async function TradesPage() {
const t = await getTranslations('trades')
const trades = await getTrades(100, 1).catch(() => [])
return (
<div className="max-w-[1400px] mx-auto px-6 py-6 pt-20">
<h1 className="text-[22px] font-medium text-white mb-5">{t('title')}</h1>
<TradesTable trades={trades} />
</div>
)
}
+53
View File
@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server'
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
async function handler(request: NextRequest): Promise<NextResponse> {
const url = new URL(request.url)
const targetPath = url.pathname.replace('/api/proxy', '')
const targetUrl = `${API_BASE}${targetPath}${url.search}`
const headers = new Headers(request.headers)
// Remove Next.js internal headers
headers.delete('host')
headers.delete('x-forwarded-for')
headers.delete('x-forwarded-host')
headers.delete('x-forwarded-proto')
const body =
request.method !== 'GET' && request.method !== 'HEAD'
? await request.arrayBuffer()
: undefined
try {
const upstream = await fetch(targetUrl, {
method: request.method,
headers,
body,
})
const responseHeaders = new Headers(upstream.headers)
responseHeaders.delete('transfer-encoding')
const responseBody = await upstream.arrayBuffer()
return new NextResponse(responseBody, {
status: upstream.status,
statusText: upstream.statusText,
headers: responseHeaders,
})
} catch (err) {
return NextResponse.json(
{ error: 'Upstream request failed', detail: String(err) },
{ status: 502 },
)
}
}
export const GET = handler
export const POST = handler
export const PUT = handler
export const PATCH = handler
export const DELETE = handler
export const HEAD = handler
export const OPTIONS = handler
+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 }
+77
View File
@@ -0,0 +1,77 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { useAccount } from 'wagmi'
import { useConnectModal } from '@rainbow-me/rainbowkit'
import { shortenAddress } from '@/lib/utils'
const navItems = [
{ key: 'overview', href: '' },
{ key: 'posts', href: '/posts' },
{ key: 'trades', href: '/trades' },
{ key: 'analytics', href: '/analytics' },
] as const
interface NavbarProps {
locale: string
}
export default function Navbar({ locale }: NavbarProps) {
const t = useTranslations('nav')
const pathname = usePathname()
const { address, isConnected } = useAccount()
const { openConnectModal } = useConnectModal()
function isActive(href: string) {
const full = `/${locale}${href}`
if (href === '') return pathname === `/${locale}` || pathname === `/${locale}/`
return pathname.startsWith(full)
}
return (
<nav className="fixed top-0 left-0 right-0 z-50 h-14 bg-[#000000] border-b border-[#141414] flex items-center px-6">
{/* Logo */}
<Link href={`/${locale}`} className="flex items-center gap-2 mr-10 shrink-0">
<span className="text-[#f97316] font-bold text-lg leading-none">TS</span>
<span className="text-white font-medium text-sm">TrumpSignal</span>
</Link>
{/* Center nav */}
<div className="flex items-center gap-6 flex-1">
{navItems.map((item) => (
<Link
key={item.key}
href={`/${locale}${item.href}`}
className={`text-[13px] transition-colors ${
isActive(item.href) ? 'text-[#f97316]' : 'text-[#555555] hover:text-white'
}`}
>
{t(item.key)}
</Link>
))}
</div>
{/* Right side */}
<div className="flex items-center gap-3 shrink-0">
<button className="text-[13px] text-[#555555] hover:text-white border border-[#141414] rounded-full px-3 py-1 transition-colors">
{t('language')}
</button>
{isConnected && address ? (
<button className="text-[13px] bg-[#141414] text-white border border-[#1a1a1a] rounded-full px-3 py-1">
{shortenAddress(address)}
</button>
) : (
<button
onClick={openConnectModal}
className="text-[13px] bg-[#f97316] text-black font-medium rounded-full px-4 py-1.5 hover:bg-[#fb923c] transition-colors"
>
{t('connectWallet')}
</button>
)}
</div>
</nav>
)
}
+147
View File
@@ -0,0 +1,147 @@
'use client'
import { useTranslations } from 'next-intl'
import type { BotTrade } from '@/types'
import Badge from '@/components/ui/Badge'
import { formatPrice, formatHold } from '@/lib/utils'
const MOCK_TRADES: BotTrade[] = [
{
id: 1,
asset: 'BTC',
side: 'long',
entry_price: 92800,
exit_price: 95100,
pnl_usd: 2300,
hold_seconds: 52 * 60,
trigger_post_id: 1,
opened_at: new Date(Date.now() - 1000 * 60 * 80).toISOString(),
closed_at: new Date(Date.now() - 1000 * 60 * 28).toISOString(),
},
{
id: 2,
asset: 'ETH',
side: 'short',
entry_price: 1920,
exit_price: 1847,
pnl_usd: 1460,
hold_seconds: 34 * 60,
trigger_post_id: 2,
opened_at: new Date(Date.now() - 1000 * 60 * 120).toISOString(),
closed_at: new Date(Date.now() - 1000 * 60 * 86).toISOString(),
},
{
id: 3,
asset: 'BTC',
side: 'long',
entry_price: 91500,
exit_price: 90200,
pnl_usd: -1300,
hold_seconds: 22 * 60,
trigger_post_id: 4,
opened_at: new Date(Date.now() - 1000 * 60 * 240).toISOString(),
closed_at: new Date(Date.now() - 1000 * 60 * 218).toISOString(),
},
{
id: 4,
asset: 'ETH',
side: 'long',
entry_price: 1780,
exit_price: 1840,
pnl_usd: 900,
hold_seconds: 8 * 60,
trigger_post_id: 1,
opened_at: new Date(Date.now() - 1000 * 60 * 360).toISOString(),
closed_at: new Date(Date.now() - 1000 * 60 * 352).toISOString(),
},
{
id: 5,
asset: 'BTC',
side: 'short',
entry_price: 93400,
exit_price: 91800,
pnl_usd: 3200,
hold_seconds: 18 * 60,
trigger_post_id: 2,
opened_at: new Date(Date.now() - 1000 * 60 * 500).toISOString(),
closed_at: new Date(Date.now() - 1000 * 60 * 482).toISOString(),
},
]
interface TradesTableProps {
trades?: BotTrade[]
}
export default function TradesTable({ trades = MOCK_TRADES }: TradesTableProps) {
const t = useTranslations('trades')
return (
<div className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] overflow-hidden">
<div className="px-5 py-4 border-b border-[#141414]">
<p className="text-[13px] font-medium text-white">{t('title')}</p>
</div>
<table className="w-full">
<thead>
<tr className="bg-[#050505]">
{[
t('asset'),
t('side'),
t('entry'),
t('exit'),
t('pnl'),
t('hold'),
t('trigger'),
].map((col) => (
<th
key={col}
className="px-5 py-3 text-left text-[11px] uppercase tracking-wider text-[#555555] font-medium"
>
{col}
</th>
))}
</tr>
</thead>
<tbody>
{trades.map((trade) => (
<tr
key={trade.id}
className="border-b border-[#141414] hover:bg-[#0d0d0d] transition-colors"
>
<td className="px-5 py-3.5">
<span className="text-[13px] font-medium text-white">{trade.asset}</span>
</td>
<td className="px-5 py-3.5">
<Badge variant={trade.side} />
</td>
<td className="px-5 py-3.5">
<span className="text-[13px] text-[#e2e8f0]">{formatPrice(trade.entry_price)}</span>
</td>
<td className="px-5 py-3.5">
<span className="text-[13px] text-[#e2e8f0]">{formatPrice(trade.exit_price)}</span>
</td>
<td className="px-5 py-3.5">
<span
className={`text-[13px] font-medium ${
trade.pnl_usd >= 0 ? 'text-[#4ade80]' : 'text-[#ef4444]'
}`}
>
{trade.pnl_usd >= 0 ? '+' : ''}${trade.pnl_usd.toLocaleString()}
</span>
</td>
<td className="px-5 py-3.5">
<span className="text-[13px] text-[#555555]">{formatHold(trade.hold_seconds)}</span>
</td>
<td className="px-5 py-3.5">
<span className="text-[12px] text-[#818cf8] hover:text-[#a5b4fc] cursor-pointer">
#{trade.trigger_post_id}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export { MOCK_TRADES }
+36
View File
@@ -0,0 +1,36 @@
type BadgeVariant = 'bullish' | 'bearish' | 'neutral' | 'long' | 'short' | 'x' | 'truth'
interface BadgeProps {
variant: BadgeVariant
children?: React.ReactNode
}
const variantStyles: Record<BadgeVariant, string> = {
bullish: 'bg-[#0d2e1a] text-[#4ade80] border border-[#1a4a2a]',
bearish: 'bg-[#2e0d0d] text-[#ef4444] border border-[#4a1a1a]',
neutral: 'bg-[#141414] text-[#555555] border border-[#1e1e1e]',
long: 'bg-[#0d2e1a] text-[#4ade80] border border-[#1a4a2a]',
short: 'bg-[#2e0d0d] text-[#ef4444] border border-[#4a1a1a]',
x: 'bg-[#141414] text-white border border-[#222222]',
truth: 'bg-[#1a1a2e] text-[#818cf8] border border-[#2a2a4a]',
}
const variantLabels: Record<BadgeVariant, string> = {
bullish: 'Bullish',
bearish: 'Bearish',
neutral: 'Neutral',
long: 'Long',
short: 'Short',
x: 'X',
truth: 'Truth',
}
export default function Badge({ variant, children }: BadgeProps) {
return (
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${variantStyles[variant]}`}
>
{children ?? variantLabels[variant]}
</span>
)
}
+16
View File
@@ -0,0 +1,16 @@
import React from 'react'
interface CardProps {
children: React.ReactNode
className?: string
}
export default function Card({ children, className = '' }: CardProps) {
return (
<div
className={`bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-5 ${className}`}
>
{children}
</div>
)
}
+23
View File
@@ -0,0 +1,23 @@
'use client'
interface PillProps {
active: boolean
onClick: () => void
children: React.ReactNode
}
export default function Pill({ active, onClick, children }: PillProps) {
return (
<button
onClick={onClick}
className={
`rounded-full px-3 py-1 text-xs transition-colors ` +
(active
? 'bg-[#141414] text-white border border-[#222222]'
: 'text-[#555555] border border-[#141414] hover:text-white')
}
>
{children}
</button>
)
}
+13
View File
@@ -0,0 +1,13 @@
import { getRequestConfig } from 'next-intl/server'
export const locales = ['en', 'ar', 'pt-BR', 'zh', 'ja'] as const
export type Locale = (typeof locales)[number]
export const defaultLocale: Locale = 'en'
export default getRequestConfig(async ({ requestLocale }) => {
const locale = (await requestLocale) ?? defaultLocale
return {
locale,
messages: (await import(`./messages/${locale}.json`)).default,
}
})
+50
View File
@@ -0,0 +1,50 @@
import type { TrumpPost, Candle, BotTrade, BotPerformance } from '@/types'
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE_URL}/api${path}`, {
headers: { 'Content-Type': 'application/json' },
...init,
})
if (!res.ok) throw new Error(`API error ${res.status}: ${path}`)
return res.json() as Promise<T>
}
export async function getPosts(limit = 20, page = 1): Promise<TrumpPost[]> {
return fetchJson<TrumpPost[]>(`/posts?limit=${limit}&page=${page}`)
}
export async function getPost(id: number): Promise<TrumpPost> {
return fetchJson<TrumpPost>(`/posts/${id}`)
}
export async function getPrices(asset: 'BTC' | 'ETH', tf: string): Promise<Candle[]> {
return fetchJson<Candle[]>(`/prices/${asset}?tf=${tf}`)
}
export async function getTrades(limit = 20, page = 1): Promise<BotTrade[]> {
return fetchJson<BotTrade[]>(`/trades?limit=${limit}&page=${page}`)
}
export async function getPerformance(): Promise<BotPerformance> {
return fetchJson<BotPerformance>(`/performance`)
}
export async function subscribe(
wallet: string,
signature: string,
): Promise<{ success: boolean; message: string }> {
return fetchJson<{ success: boolean; message: string }>(`/subscribe`, {
method: 'POST',
body: JSON.stringify({ wallet, signature }),
})
}
export async function getUser(
wallet: string,
): Promise<{ wallet_address: string; active: boolean; subscribed_at: string | null; hl_api_key_set: boolean; trades: BotTrade[] }> {
return fetchJson<{ wallet_address: string; active: boolean; subscribed_at: string | null; hl_api_key_set: boolean; trades: BotTrade[] }>(
`/user/${wallet}`,
)
}
+53
View File
@@ -0,0 +1,53 @@
'use client'
import { useEffect, useRef, useCallback } from 'react'
const WS_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000'
type PriceUpdate = { type: 'price'; asset: 'BTC' | 'ETH'; price: number }
type PostUpdate = { type: 'new_post'; post: object }
type WsMessage = PriceUpdate | PostUpdate
interface Handlers {
onPrice?: (asset: 'BTC' | 'ETH', price: number) => void
onNewPost?: (post: object) => void
}
export function usePriceSocket(handlers: Handlers) {
const wsRef = useRef<WebSocket | null>(null)
const handlersRef = useRef(handlers)
handlersRef.current = handlers
const connect = useCallback(() => {
const ws = new WebSocket(`${WS_URL}/ws/prices`)
wsRef.current = ws
ws.onmessage = (e) => {
try {
const msg: WsMessage = JSON.parse(e.data)
if (msg.type === 'price' && handlersRef.current.onPrice) {
handlersRef.current.onPrice(msg.asset, msg.price)
} else if (msg.type === 'new_post' && handlersRef.current.onNewPost) {
handlersRef.current.onNewPost(msg.post)
}
} catch {
// ignore malformed messages
}
}
ws.onclose = () => {
setTimeout(connect, 3000)
}
ws.onerror = () => {
ws.close()
}
}, [])
useEffect(() => {
connect()
return () => {
wsRef.current?.close()
}
}, [connect])
}
+33
View File
@@ -0,0 +1,33 @@
export function formatPrice(n: number): string {
return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
export function formatPct(n: number): string {
const sign = n >= 0 ? '+' : ''
return `${sign}${n.toFixed(2)}%`
}
export function formatHold(seconds: number): string {
if (seconds < 60) return `${seconds}s`
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
if (h === 0) return `${m}m`
if (m === 0) return `${h}h`
return `${h}h ${m}m`
}
export function shortenAddress(addr: string): string {
if (addr.length < 10) return addr
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
}
export function sentimentColor(s: string): string {
switch (s) {
case 'bullish':
return '#4ade80'
case 'bearish':
return '#ef4444'
default:
return '#555555'
}
}
+13
View File
@@ -0,0 +1,13 @@
import { createConfig, http } from 'wagmi'
import { mainnet } from 'wagmi/chains'
import { metaMask } from 'wagmi/connectors'
export const chains = [mainnet] as const
export const config = createConfig({
chains,
connectors: [metaMask()],
transports: {
[mainnet.id]: http(),
},
})
+68
View File
@@ -0,0 +1,68 @@
{
"nav": {
"overview": "Overview",
"posts": "Posts",
"trades": "Trades",
"analytics": "Analytics",
"settings": "Settings",
"connectWallet": "Connect wallet",
"language": "EN"
},
"kpi": {
"btcPrice": "BTC Price",
"ethPrice": "ETH Price",
"postsTracked": "Posts Tracked",
"avgMove": "Avg Move"
},
"bot": {
"title": "Bot Performance",
"winRate": "Win Rate",
"netPnl": "Net PnL",
"totalTrades": "Trades",
"avgHold": "Avg Hold",
"connectCta": "Let our AI trade for you",
"connectDesc": "Connect your wallet to enable automated trading signals based on Trump's social posts.",
"subscribeBtn": "Subscribe — $99/mo",
"settingsTitle": "Bot Settings",
"apiKeyLabel": "API Key",
"apiKeyPlaceholder": "Paste your exchange API key",
"connectWalletFree": "Connect wallet — free",
"activeStatus": "Active",
"saveKey": "Save",
"period": "30d"
},
"trades": {
"title": "Recent Bot Trades",
"asset": "Asset",
"side": "Side",
"entry": "Entry",
"exit": "Exit",
"pnl": "PnL",
"hold": "Hold",
"trigger": "Trigger"
},
"posts": {
"title": "Trump Posts",
"sentimentLabel": "Sentiment",
"sourceLabel": "Source",
"confidenceLabel": "AI Confidence",
"impactLabel": "Price Impact",
"comingSoon": "Full post feed coming soon."
},
"analytics": {
"title": "Analytics",
"comingSoon": "Analytics coming soon."
},
"settings": {
"title": "Settings",
"connectRequired": "Connect your wallet to access settings.",
"comingSoon": "Settings coming soon."
},
"common": {
"bullish": "Bullish",
"bearish": "Bearish",
"neutral": "Neutral",
"loading": "Loading...",
"comingSoon": "Coming soon"
}
}
+13
View File
@@ -0,0 +1,13 @@
import createMiddleware from 'next-intl/middleware'
import { locales, defaultLocale } from './i18n'
export default createMiddleware({
locales,
defaultLocale,
})
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)' ,
],
}
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+10
View File
@@ -0,0 +1,10 @@
import createNextIntlPlugin from 'next-intl/plugin'
const withNextIntl = createNextIntlPlugin('./i18n.ts')
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
export default withNextIntl(nextConfig)
+13479
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
{
"name": "trumpsignal",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start -p 3001",
"lint": "next lint"
},
"dependencies": {
"next": "14.2.5",
"react": "^18",
"react-dom": "^18",
"next-intl": "^3.15.3",
"lightweight-charts": "^4.1.3",
"wagmi": "^2.10.5",
"viem": "^2.17.0",
"@rainbow-me/rainbowkit": "^2.1.3",
"@tanstack/react-query": "^5.40.0",
"zustand": "^4.5.3"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "^5",
"tailwindcss": "^3.4.4",
"postcss": "^8",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "14.2.5"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+32
View File
@@ -0,0 +1,32 @@
import { create } from 'zustand'
interface DashboardState {
selectedPostId: number | null
asset: 'BTC' | 'ETH'
timeframe: '5m' | '15m' | '1H' | '4H' | '1D' | '1W'
walletAddress: string | null
isSubscribed: boolean
livePrices: { BTC: number | null; ETH: number | null }
setSelectedPost: (id: number | null) => void
setAsset: (asset: 'BTC' | 'ETH') => void
setTimeframe: (tf: '5m' | '15m' | '1H' | '4H' | '1D' | '1W') => void
setWallet: (address: string | null) => void
setSubscribed: (subscribed: boolean) => void
setLivePrice: (asset: 'BTC' | 'ETH', price: number) => void
}
export const useDashboardStore = create<DashboardState>((set) => ({
selectedPostId: null,
asset: 'BTC',
timeframe: '4H',
walletAddress: null,
isSubscribed: false,
livePrices: { BTC: null, ETH: null },
setSelectedPost: (id) => set({ selectedPostId: id }),
setAsset: (asset) => set({ asset }),
setTimeframe: (timeframe) => set({ timeframe }),
setWallet: (walletAddress) => set({ walletAddress }),
setSubscribed: (isSubscribed) => set({ isSubscribed }),
setLivePrice: (asset, price) =>
set((s) => ({ livePrices: { ...s.livePrices, [asset]: price } })),
}))
+33
View File
@@ -0,0 +1,33 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./app/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
],
theme: {
extend: {
colors: {
'bg-base': '#000000',
'bg-card': '#0a0a0a',
'bg-deep': '#050505',
'bg-hover': '#0d0d0d',
'bg-input': '#060606',
'border-default': '#141414',
'border-hover': '#222222',
'text-primary': '#ffffff',
'text-secondary': '#e2e8f0',
'text-muted': '#555555',
'text-faint': '#333333',
accent: '#f97316',
'accent-hover': '#fb923c',
green: '#4ade80',
red: '#ef4444',
purple: '#818cf8',
},
},
},
plugins: [],
}
export default config
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+49
View File
@@ -0,0 +1,49 @@
export interface TrumpPost {
id: number
text: string
source: 'x' | 'truth'
published_at: string
sentiment: 'bullish' | 'bearish' | 'neutral'
signal: 'buy' | 'sell' | 'short' | 'hold' | null
ai_confidence: number
ai_reasoning: string | null
relevant: boolean
price_impact: {
asset: 'BTC' | 'ETH'
m5: number
m15: number
m1h: number
price_at_post: number
} | null
}
export interface Candle {
time: number
open: number
high: number
low: number
close: number
volume: number
}
export interface BotTrade {
id: number
asset: 'BTC' | 'ETH'
side: 'long' | 'short'
entry_price: number
exit_price: number
pnl_usd: number
hold_seconds: number
trigger_post_id: number
opened_at: string
closed_at: string
}
export interface BotPerformance {
period_days: number
total_trades: number
win_rate: number
net_pnl_usd: number
avg_hold_seconds: number
max_drawdown_pct: number
}