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:
k
2026-06-09 22:55:27 +08:00
parent 9e0f6554cb
commit 4c3c8c6f87
57 changed files with 3464 additions and 1855 deletions
+71 -25
View File
@@ -5,8 +5,8 @@ import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useLocale } from 'next-intl'
import { useAccount, useSignMessage } from 'wagmi'
import type { BotTrade, TrumpPost } from '@/types'
import { getTrades, getPosts } from '@/lib/api'
import type { BotTrade } from '@/types'
import { getTrades, getUserPublic } from '@/lib/api'
import { useDashboardStore } from '@/store/dashboard'
import { getCachedViewEnvelope, getOrCreateViewEnvelope, type SignedEnvelope } from '@/lib/signedRequest'
import TradeTable from '@/components/trades/TradeTable'
@@ -18,13 +18,13 @@ export default function TradesPageClient() {
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
const { address, isConnected } = useAccount()
const { signMessageAsync } = useSignMessage()
const { isSubscribed, hlApiKeySet } = useDashboardStore()
const { isSubscribed, hlApiKeySet, paperMode,
setSubscribed, setHlApiKeySet, setPaperMode, setBotReadiness } = useDashboardStore()
const pathname = usePathname()
const locale = pathname.split('/')[1] || 'en'
const [mounted, setMounted] = useState(false)
const [trades, setTrades] = useState<BotTrade[]>([])
const [posts, setPosts] = useState<TrumpPost[]>([])
const [loading, setLoading] = useState(true)
const [loadErr, setLoadErr] = useState('')
const [needsUnlock, setNeedsUnlock] = useState(false)
@@ -37,41 +37,77 @@ export default function TradesPageClient() {
return () => { aliveRef.current = false }
}, [])
// genRef guards loadAll against stale-closure wallet-switch races.
// `snapAddr !== address` inside an async function compares two copies of
// the same closure-captured value — it's always equal even when the wallet
// has changed, so the check is a no-op. genRef is a mutable ref that every
// closure can read, so `gen !== genRef.current` correctly detects staleness.
const genRef = useRef(0)
useEffect(() => { genRef.current++ }, [address])
// B41: TradesPageClient can be the entry point (direct navigation to /trades)
// without DashboardClient or BotConfigPanel ever running. In that case the
// Zustand store has isSubscribed=false (initial default), causing the
// "Bot not configured" banner to appear for valid subscribers.
// Fix: fetch /user/{wallet}/public here too — it's lightweight, unauthenticated,
// and idempotent with the same fetch DashboardClient does.
useEffect(() => {
if (!address || !isConnected) return
let cancelled = false
const snap = address
getUserPublic(snap.toLowerCase())
.then(u => {
if (cancelled || snap !== address) return
setSubscribed(u.active)
setHlApiKeySet(u.hl_api_key_set)
setBotReadiness(u.hl_api_key_set ? 'saved' : 'unknown')
setPaperMode(!!u.paper_mode)
})
.catch(() => {})
return () => { cancelled = true }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [address, isConnected])
// Load public posts always; load private trades only if we have a view
// envelope. `forcedEnv` lets the in-page Unlock button pass a freshly-minted
// one so the user never has to detour through the Settings page first.
async function loadAll(forcedEnv?: SignedEnvelope) {
if (!address || !isConnected) {
setTrades([]); setPosts([]); setLoading(false); setLoadErr(''); setNeedsUnlock(false)
setTrades([]); setLoading(false); setLoadErr(''); setNeedsUnlock(false)
return
}
const gen = genRef.current
const snapAddr = address // for API calls only — NOT for stale detection
setLoading(true)
const env = forcedEnv
?? getCachedViewEnvelope('view_trades', address)
?? getCachedViewEnvelope('view_user', address)
?? getCachedViewEnvelope('view_trades', snapAddr)
?? getCachedViewEnvelope('view_user', snapAddr)
let failed = false
try {
const [t, p] = await Promise.all([
env
? getTrades(address, env, 100, 1).catch(e => {
failed = true
setLoadErr(e instanceof Error ? e.message : 'Failed to load trades')
return [] as BotTrade[]
})
: Promise.resolve([] as BotTrade[]),
getPosts(500, 1).catch(() => [] as TrumpPost[]),
])
if (!aliveRef.current) return
const t = env
? await getTrades(snapAddr, env, 500, 1).catch(e => {
failed = true
setLoadErr(e instanceof Error ? e.message : 'Failed to load trades')
return [] as BotTrade[]
})
: []
// Use genRef — NOT `snapAddr !== address` (stale closure: both are the
// same closed-over value and the comparison is always false).
if (!aliveRef.current || gen !== genRef.current) return
setTrades(t)
setPosts(p)
setNeedsUnlock(!env)
if (env && !failed) setLoadErr('')
} finally {
if (aliveRef.current) setLoading(false)
if (aliveRef.current && gen === genRef.current) setLoading(false)
}
}
useEffect(() => {
// B27/B35: clear stale previous-wallet data BEFORE the async fetch so the
// UI never briefly shows another wallet's private trades.
setTrades([])
setNeedsUnlock(false)
setLoadErr('')
// On navigation we only use a cached envelope — never auto-popup the
// wallet. If none is cached the user unlocks explicitly via the button.
void loadAll()
@@ -84,8 +120,12 @@ export default function TradesPageClient() {
setUnlocking(true)
setLoadErr('')
try {
// view_user is a superset accepted by /trades, /positions/open,
// /positions/today — one signature unlocks everything on this page.
// Previously view_trades was used here, but that left OpenPositions
// locked (it requires view_positions or view_user).
const env = await getOrCreateViewEnvelope({
action: 'view_trades', wallet: address, signMessageAsync,
action: 'view_user', wallet: address, signMessageAsync,
})
await loadAll(env)
} catch (e) {
@@ -97,7 +137,14 @@ export default function TradesPageClient() {
}
}
const needsSetup = mounted && isConnected && (!isSubscribed || !hlApiKeySet)
// B40: paper users have no HL key but the bot IS configured for them — don't
// show "Bot not configured" just because hlApiKeySet is false.
// B41: on cold start (navigating directly to /trades), isSubscribed starts
// false in the store until DashboardClient or BotConfigPanel fetches
// /user/public. Guard with !loading so the banner only appears once
// the page has confirmed the wallet is genuinely unconfigured.
const needsSetup = mounted && isConnected && !loading &&
(!isSubscribed || (!hlApiKeySet && !paperMode))
return (
<div className="page">
@@ -105,8 +152,7 @@ export default function TradesPageClient() {
<div>
<h1 className="page-title">{isZh ? '交易执行' : 'Trades'}</h1>
<PageHint>
What the bot actually did with your money currently-open positions
on top, closed-trade history with realized P&amp;L below.
Open positions above · closed trade history with realized P&amp;L below.
</PageHint>
</div>
</div>
@@ -163,7 +209,7 @@ export default function TradesPageClient() {
</div>
)}
<TradeTable trades={trades} posts={posts} loading={loading} />
<TradeTable trades={trades} loading={loading} locked={needsUnlock} />
</div>
)
}