KOL count: 29 → 25 across marketing/SEO copy
Backend KOL_FEEDS trimmed from 29 to 25 (dead feeds removed).
Sync all hardcoded count mentions:
- layout.tsx JSON-LD, page.tsx (metric + comparison + copy)
- kol/page.tsx, KolPageClient.tsx ("and 26 more" → "and 22 more")
- glossary/page.tsx, opengraph-image.tsx
- public/llms.txt, llms-full.txt
- drop removed KOLs (Dragonfly Capital, Nic Carter) from named lists
Bundles other in-flight frontend work already in the working tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { useLocale } from 'next-intl'
|
||||
import type { BotTrade, TrumpPost } from '@/types'
|
||||
import type { BotTrade } from '@/types'
|
||||
import Pagination from '@/components/ui/Pagination'
|
||||
|
||||
// ── Formatters ────────────────────────────────────────────────────────────────
|
||||
@@ -16,7 +16,8 @@ function fmtMoney(n: number, opts: { sign?: boolean; decimals?: number } = {}) {
|
||||
return '$' + s
|
||||
}
|
||||
function fmtPct(n: number) { return (n >= 0 ? '+' : '') + n.toFixed(2) + '%' }
|
||||
function fmtHold(s: number) {
|
||||
function fmtHold(s: number | null | undefined) {
|
||||
if (s == null) return '—'
|
||||
if (s < 60) return s + 's'
|
||||
const m = Math.floor(s / 60)
|
||||
if (m < 60) return m + 'm'
|
||||
@@ -24,18 +25,36 @@ function fmtHold(s: number) {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
trades: BotTrade[]
|
||||
posts: TrumpPost[]
|
||||
loading: boolean
|
||||
trades: BotTrade[]
|
||||
loading: boolean
|
||||
locked?: boolean // true = waiting for wallet signature, not "no trades"
|
||||
}
|
||||
|
||||
// ASSETS filter is derived dynamically from the trade set (see useMemo below)
|
||||
// so new assets (TRUMP, BNB, DOGE, LINK, AAVE, ...) appear automatically.
|
||||
const SIDES = ['all', 'long', 'short'] as const
|
||||
|
||||
// Human-readable labels for raw signal-source identifiers. Keeps the trade
|
||||
// history readable for non-technical users (matches PostCards source labels).
|
||||
const SOURCE_LABEL: Record<string, string> = {
|
||||
truth: 'Trump',
|
||||
btc_bottom_reversal: 'BTC Macro Bottom',
|
||||
funding_reversal: 'Funding Reversal',
|
||||
kol_divergence: 'KOL Divergence',
|
||||
sma_reclaim: 'SMA Reclaim',
|
||||
rsi_reversal: 'RSI Reversal',
|
||||
breakout: 'Breakout',
|
||||
adopted: 'Adopted',
|
||||
manual: 'Manual',
|
||||
unknown: 'Unknown',
|
||||
}
|
||||
function sourceLabel(src: string): string {
|
||||
return SOURCE_LABEL[src?.toLowerCase()] ?? src
|
||||
}
|
||||
|
||||
const TRADES_PER_PAGE = 25
|
||||
|
||||
export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
export default function TradeTable({ trades, loading, locked = false }: Props) {
|
||||
const locale = useLocale()
|
||||
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
|
||||
const [assetFilter, setAssetFilter] = useState('all')
|
||||
@@ -63,12 +82,31 @@ export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
return Array.from(set).sort()
|
||||
}, [trades])
|
||||
|
||||
// Reset filters that no longer apply to the loaded trade set. On a wallet
|
||||
// switch the trades prop changes but the local filter state persists, so a
|
||||
// source/asset/side the previous wallet had could stay "stuck" — and when
|
||||
// the new wallet has a single source the breakdown card (sources.length > 1)
|
||||
// disappears, removing the only Clear affordance. Clamping here guarantees
|
||||
// the user always sees their full new history.
|
||||
useEffect(() => {
|
||||
if (sourceFilter !== 'all' && !sources.includes(sourceFilter)) setSourceFilter('all')
|
||||
if (assetFilter !== 'all' && !assets.includes(assetFilter)) setAssetFilter('all')
|
||||
}, [sources, assets, sourceFilter, assetFilter])
|
||||
|
||||
// Per-source PnL aggregate — the critical view for "which module makes money".
|
||||
// Computed BEFORE the asset/side filter so the source breakdown reflects the
|
||||
// full universe, not whatever sub-filter is currently applied.
|
||||
// Apply asset/side/paper filters for the source breakdown so the cards stay
|
||||
// consistent with the KPI and table rows below. We intentionally do NOT apply
|
||||
// sourceFilter here — filtering by source would trivially make one card 100%.
|
||||
const filteredForSources = useMemo(() => trades.filter(t => {
|
||||
if (assetFilter !== 'all' && t.asset !== assetFilter) return false
|
||||
if (sideFilter !== 'all' && t.side !== sideFilter) return false
|
||||
if (hidePaper && t.is_paper) return false
|
||||
return true
|
||||
}), [trades, assetFilter, sideFilter, hidePaper])
|
||||
|
||||
const perSource = useMemo(() => {
|
||||
const acc: Record<string, { trades: number; pnl: number; wins: number; paper: number }> = {}
|
||||
for (const t of trades) {
|
||||
for (const t of filteredForSources) {
|
||||
const k = t.trigger_source || 'unknown'
|
||||
if (!acc[k]) acc[k] = { trades: 0, pnl: 0, wins: 0, paper: 0 }
|
||||
acc[k].trades += 1
|
||||
@@ -79,7 +117,7 @@ export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
if (t.is_paper) acc[k].paper += 1
|
||||
}
|
||||
return acc
|
||||
}, [trades])
|
||||
}, [filteredForSources])
|
||||
|
||||
const filtered = useMemo(() => trades.filter(t => {
|
||||
if (sourceFilter !== 'all' && (t.trigger_source || 'unknown') !== sourceFilter) return false
|
||||
@@ -98,8 +136,9 @@ export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
const totalPnl = priced.reduce((s, t) => s + (t.pnl_usd ?? 0), 0)
|
||||
const wins = priced.filter(t => (t.pnl_usd ?? 0) > 0).length
|
||||
const losses = priced.length - wins
|
||||
const avgHold = filtered.length > 0
|
||||
? Math.round(filtered.reduce((s, t) => s + t.hold_seconds, 0) / filtered.length)
|
||||
const withHold = filtered.filter(t => t.hold_seconds !== null)
|
||||
const avgHold = withHold.length > 0
|
||||
? Math.round(withHold.reduce((s, t) => s + (t.hold_seconds ?? 0), 0) / withHold.length)
|
||||
: 0
|
||||
|
||||
return (
|
||||
@@ -111,7 +150,7 @@ export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
fontSize: 11, fontWeight: 600, letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase', color: 'var(--ink-3)', marginBottom: 10,
|
||||
}}>
|
||||
{isZh ? '按信号来源拆分盈亏' : 'P&L by signal source'}
|
||||
{isZh ? '按信号来源拆分盈亏' : 'Which signal makes money'}
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
@@ -142,7 +181,7 @@ export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
fontSize: 11, fontWeight: 600, color: 'var(--ink-2)',
|
||||
marginBottom: 4, overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
}}>
|
||||
{src}
|
||||
{sourceLabel(src)}
|
||||
{s.paper > 0 && (
|
||||
<span style={{
|
||||
fontSize: 9, marginLeft: 6, padding: '1px 5px',
|
||||
@@ -171,7 +210,7 @@ export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
border: '1px solid var(--line)', borderRadius: 4,
|
||||
background: 'transparent', cursor: 'pointer', color: 'var(--ink-2)' }}
|
||||
>
|
||||
{isZh ? `清除(当前为 “${sourceFilter}”)` : `Clear (showing “${sourceFilter}”)`}
|
||||
{isZh ? `清除(当前为 “${sourceLabel(sourceFilter)}”)` : `Clear (showing “${sourceLabel(sourceFilter)}”)`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -255,7 +294,7 @@ export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
<th>{isZh ? '开仓' : 'Entry'}</th>
|
||||
<th>{isZh ? '平仓' : 'Exit'}</th>
|
||||
<th>{isZh ? '持仓' : 'Hold'}</th>
|
||||
<th>{isZh ? '触发内容' : 'Trigger'}</th>
|
||||
<th>{isZh ? '触发信号' : 'What triggered it'}</th>
|
||||
<th>P&L</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -263,12 +302,13 @@ export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
{filtered.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} style={{ textAlign: 'center', padding: 40, color: 'var(--ink-3)' }}>
|
||||
{isZh ? '没有符合条件的交易。' : 'No trades found'}
|
||||
{locked
|
||||
? (isZh ? '签名解锁后即可查看交易历史。' : 'Sign to unlock your trade history above.')
|
||||
: (isZh ? '没有符合条件的交易。' : 'No trades found')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{pageRows.map(t => {
|
||||
const tp = posts.find(p => p.id === t.trigger_post_id)
|
||||
const roi =
|
||||
t.entry_price && t.exit_price != null
|
||||
? ((t.exit_price - t.entry_price) / t.entry_price) * 100 * (t.side === 'long' ? 1 : -1)
|
||||
@@ -283,7 +323,7 @@ export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
padding: '2px 7px', borderRadius: 4,
|
||||
background: 'var(--bg-sunk)',
|
||||
}}>
|
||||
{src}
|
||||
{sourceLabel(src)}
|
||||
</span>
|
||||
{t.is_paper && (
|
||||
<span style={{
|
||||
@@ -310,9 +350,9 @@ export default function TradeTable({ trades, posts, loading }: Props) {
|
||||
<td className="mono">{t.exit_price != null ? '$' + t.exit_price.toLocaleString() : '—'}</td>
|
||||
<td className="mono" style={{ color: 'var(--ink-2)' }}>{fmtHold(t.hold_seconds)}</td>
|
||||
<td style={{ maxWidth: 260 }}>
|
||||
{tp ? (
|
||||
{t.trigger_post_text ? (
|
||||
<span style={{ fontSize: 12, color: 'var(--ink-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>
|
||||
{tp.text.slice(0, 60)}…
|
||||
{t.trigger_post_text.slice(0, 60)}…
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 12, color: 'var(--ink-4)' }}>—</span>
|
||||
|
||||
Reference in New Issue
Block a user