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
+73 -21
View File
@@ -12,6 +12,7 @@
*/
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { useLocale } from 'next-intl'
import { useAccount, useSignMessage } from 'wagmi'
import {
@@ -22,7 +23,7 @@ import {
type OpenPosition,
type TodayStats,
} from '@/lib/api'
import { getCachedViewEnvelope, signRequest } from '@/lib/signedRequest'
import { getCachedViewEnvelope, getOrCreateViewEnvelope, signRequest, type SignedEnvelope } from '@/lib/signedRequest'
import { isUserRejection, walletErrorLabel } from '@/lib/walletError'
const POLL_MS = 15_000
@@ -85,7 +86,7 @@ function PositionRow({ p, onClose, onToggleGrow, growBusy, isZh }: {
</span>
{/* Entry → Current */}
<div className="mono" style={{ fontSize: 12 }}>
<div style={{ color: 'var(--ink-3)', fontSize: 10 }}>entry mark</div>
<div style={{ color: 'var(--ink-3)', fontSize: 10 }}>entry / current price</div>
<div>
{fmtMoney(p.entry_price)}
<span style={{ color: 'var(--ink-4)' }}> </span>
@@ -102,10 +103,10 @@ function PositionRow({ p, onClose, onToggleGrow, growBusy, isZh }: {
{((p.derisk_steps ?? 0) > 0 || (p.addon_steps ?? 0) > 0) && (
<div style={{ fontSize: 9, marginTop: 2 }}>
{(p.addon_steps ?? 0) > 0 && (
<span style={{ color: 'var(--up)' }}>{`⬆ pyramided ×${p.addon_steps} `}</span>
<span style={{ color: 'var(--up)' }}>{`↑ scaled in ×${p.addon_steps} `}</span>
)}
{(p.derisk_steps ?? 0) > 0 && (
<span style={{ color: 'var(--down)' }}>{`⬇ de-risked ×${p.derisk_steps}`}</span>
<span style={{ color: 'var(--down)' }}>{`↓ trimmed ×${p.derisk_steps}`}</span>
)}
</div>
)}
@@ -114,8 +115,8 @@ function PositionRow({ p, onClose, onToggleGrow, growBusy, isZh }: {
onClick={() => onToggleGrow(p)}
disabled={growBusy}
title={p.grow_mode
? (isZh ? 'Grow 已开启:趋势确认后会继续顺势加仓。点击关闭。' : 'Grow ON — scales INTO this winner on confirmed trend. Click to turn off.')
: (isZh ? 'Grow 已关闭:仅持有并做保护性降风险。点击后允许顺势加仓。' : 'Grow OFF — hold + protective de-risk only. Click to let it pyramid.')}
? (isZh ? '加仓已开启:趋势确认后自动追加仓。点击关闭。' : 'Scale-in ON — bot adds to this position when trend is confirmed. Click to turn off.')
: (isZh ? '加仓已关闭:仅持有并做保护性仓。' : 'Scale-in OFF — hold + protective trim only. Click to allow adding.')}
style={{
marginTop: 4, fontSize: 9, fontWeight: 700, padding: '2px 7px',
borderRadius: 999, cursor: growBusy ? 'wait' : 'pointer',
@@ -124,7 +125,7 @@ function PositionRow({ p, onClose, onToggleGrow, growBusy, isZh }: {
color: p.grow_mode ? '#fff' : 'var(--ink-3)',
}}
>
{p.grow_mode ? '⬆ Grow ON' : 'Grow OFF'}
{p.grow_mode ? '↑ Scale-in ON' : 'Scale-in OFF'}
</button>
</div>
{/* Hold time */}
@@ -141,7 +142,7 @@ function PositionRow({ p, onClose, onToggleGrow, growBusy, isZh }: {
</div>
{p.realized_usd != null && p.realized_usd !== 0 && (
<div style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 1 }}>
locked in {fmtMoney(p.realized_usd, { sign: true })}
banked {fmtMoney(p.realized_usd, { sign: true })} from partial close
</div>
)}
</div>
@@ -172,6 +173,7 @@ export default function OpenPositions() {
const [today, setToday] = useState<TodayStats | null>(null)
const [err, setErr] = useState<string | null>(null)
const [needsUnlock, setNeedsUnlock] = useState(false)
const [unlocking, setUnlocking] = useState(false)
// Close-confirmation modal state
const [closing, setClosing] = useState<OpenPosition | null>(null)
const [closeState, setCloseState] = useState<'idle'|'signing'|'closing'|'err'>('idle')
@@ -212,10 +214,11 @@ export default function OpenPositions() {
}
useEffect(() => {
if (!isConnected || !address) {
setPositions(null); setToday(null); setNeedsUnlock(false); setErr(null)
return
}
// B39: wipe stale data from the previous wallet immediately — don't wait
// for the async fetch to complete before the UI shows a clean slate.
setPositions(null); setToday(null); setNeedsUnlock(false); setErr(null)
if (!isConnected || !address) return
let cancelled = false
// Only reuse a cached envelope here. Open positions should never trigger
@@ -253,6 +256,31 @@ export default function OpenPositions() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [address, isConnected])
// In-page unlock: mint a view_user envelope (one signature) right here and
// load positions immediately — no detour to the Settings page. view_user is
// a superset accepted by /positions/open and /positions/today.
async function handleUnlock() {
if (!address || unlocking) return
setUnlocking(true); setErr(null)
try {
const env = await getOrCreateViewEnvelope({
action: 'view_user', wallet: address, signMessageAsync,
})
const [p, t] = await Promise.all([
getOpenPositions(address, env),
getTodayStats(address, env),
])
setPositions(p.positions); setToday(t); setNeedsUnlock(false); setErr(null)
} catch (e: unknown) {
// User-cancelled signature is benign — keep the unlock CTA visible.
if (!isUserRejection(e)) {
setErr(e instanceof Error ? e.message.slice(0, 80) : (isZh ? '解锁失败' : 'unlock failed'))
}
} finally {
setUnlocking(false)
}
}
// Trigger close. Two-step: opens modal, user confirms, we sign + POST.
async function confirmCloseTrade() {
if (!address || !closing) return
@@ -293,15 +321,39 @@ export default function OpenPositions() {
Open positions
</div>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink)' }}>
Sign in once to view open positions
Sign in to see open positions
</div>
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 4, lineHeight: 1.6 }}>
Go to the <strong>Settings page</strong> and click <em>Sign in to view your settings</em>. After signing, your open positions will appear here automatically.
Sign once on this device to unlock your positions (valid for a few
minutes, no transaction, no gas).
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 10, flexWrap: 'wrap' }}>
<button className="btn amber" style={{ fontSize: 13, padding: '8px 16px' }}
disabled={unlocking} onClick={handleUnlock}>
{unlocking ? 'Waiting for signature…' : 'Unlock positions'}
</button>
<Link href={`/${locale}/settings`} style={{ fontSize: 12, color: 'var(--ink-3)', textDecoration: 'none' }}>
or go to Settings
</Link>
</div>
{err && (
<div style={{ fontSize: 11, color: 'var(--down)', marginTop: 8 }}> {err}</div>
)}
</div>
)
}
if (positions === null && today === null) return null
// When the first load fails, positions and today are still null but err is set.
// Without this guard the component returns null (blank), swallowing the error.
if (positions === null && today === null) {
if (err) {
return (
<div className="card" style={{ padding: '12px 16px', marginBottom: 16, fontSize: 12, color: 'var(--down)' }}>
Could not load positions {err}
</div>
)
}
return null
}
const totalUnrealized = (positions ?? []).reduce(
(s, p) => s + (p.unrealized_usd ?? 0), 0,
@@ -363,7 +415,7 @@ export default function OpenPositions() {
</div>
{today != null && (today.open_realized_usd ?? 0) !== 0 && (
<div style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 1 }}>
{`incl. ${fmtMoney(today.open_realized_usd, { sign: true })} locked in on open trades`}
{`incl. ${fmtMoney(today.open_realized_usd, { sign: true })} banked from partial closes`}
</div>
)}
</div>
@@ -447,8 +499,8 @@ export default function OpenPositions() {
</div>
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 16 }}>
{closing.is_paper
? (isZh ? '这是模拟交易,平仓只做本地记录,不会调用 Hyperliquid。' : 'Paper trade — synthetic close, no Hyperliquid call.')
: (isZh ? '会向 Hyperliquid 发送 IOC 市价单。已实现盈亏将立即确认。' : 'Sends an IOC market order to Hyperliquid. Realised PnL is permanent.')}
? (isZh ? '这是模拟交易,平仓只做本地记录,不会动用真实资金。' : 'Paper trade — simulated close, no real funds involved.')
: (isZh ? '立即按市价在 Hyperliquid 上平仓。盈亏将立即结算,无法撤销。' : 'Closes at market price on Hyperliquid right now. The profit or loss becomes final and cannot be undone.')}
</div>
<div style={{
background: 'var(--bg-sunk)', borderRadius: 8, padding: 14, marginBottom: 16,
@@ -458,17 +510,17 @@ export default function OpenPositions() {
<span className="mono">{fmtMoney(closing.entry_price)}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 6 }}>
<span style={{ color: 'var(--ink-3)' }}>{isZh ? '现价' : 'Mark'}</span>
<span style={{ color: 'var(--ink-3)' }}>{isZh ? '现价' : 'Current price'}</span>
<span className="mono">{closing.current_price != null ? fmtMoney(closing.current_price) : (isZh ? '暂无' : 'n/a')}</span>
</div>
{closing.realized_usd != null && closing.realized_usd !== 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 6, color: 'var(--ink-3)' }}>
<span>{isZh ? '已锁定(降风险' : 'Locked in (de-risked)'}</span>
<span>{isZh ? '已落袋(之前减仓' : 'Already banked'}</span>
<span className="mono">{fmtMoney(closing.realized_usd, { sign: true })}</span>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, fontWeight: 600, marginTop: 8, paddingTop: 8, borderTop: '1px solid var(--line)' }}>
<span>{isZh ? '当前未平部分预估盈亏' : 'Est. PnL on open portion'}</span>
<span>{isZh ? '平仓后将结算' : 'You\'ll get if you close now'}</span>
<span style={{
color: (closing.unrealized_usd ?? 0) > 0 ? 'var(--up)'
: (closing.unrealized_usd ?? 0) < 0 ? 'var(--down)' : 'var(--ink-2)',