Production polish: i18n shelved, PWA icons, SEO/GEO cleanup, UX fixes

Big-picture changes since 01be8e7:

New routes — /trump /btc /kol /trades /analytics /archive plus four
SEO landing pages (/methodology /glossary /case-studies /contact),
all served under /[locale]. Each has dedicated metadata + JSON-LD.

KOL page (new) — DigestWidget + OnchainWidget + TalkVsTradesWidget,
filter by handle/ticker, click-through to per-post detail with the
original AI-extracted ticker/conviction/quote.

BTC page (new) — tabbed: macro-bottom (AHR999 + 200WMA + Pi Cycle)
and funding-rate reversal, with live sparkline + threshold bands.

Telegram card on Settings — wallet-link code generation, status,
disconnect. Preferences moved into the bot itself (/trump /btc etc.)
so the card stays minimal.

SignalMonitor (new) — ETH/LINK Bollinger breakout monitor in its own
component, shares the singleton WsProvider so no second WS opens.

WS singleton refactor (lib/wsContext) — shared WsProvider + useWsSubscribe
hook. Cleanup now actively closes the socket on unmount; previously the
local `ws` couldn't be reached from cleanup and leaked one connection
per StrictMode remount.

OpenPositions polling no longer pops MetaMask in the background —
splits into load('first') for user-initiated and load('poll') that
uses getCachedViewEnvelope without signing.

i18n shelved — proxy.ts (Next 16 middleware rename) wires next-intl
but only Navbar + layout footer have translations. Rest of UI used
isZh ternary scattered across 28 files. All `const isZh = locale === 'zh'`
flipped to `const isZh = false` so every site renders English; Chinese
branches kept as dead code so revival is one regex away.
LanguageSwitch hidden but file kept. zh-CN hreflang removed from
metadata + sitemap to avoid duplicate-content penalties.

Wallet error handling — lib/walletError.ts: isUserRejection walks EIP-1193
code 4001 + .cause chain; previous string-match for "reject"/"denied"
broke for users running MetaMask in non-English UIs. 12 call sites
migrated across 5 components.

PWA icons — app/icon.tsx + app/apple-icon.tsx render the brand "α" via
Next's ImageResponse so no static PNG asset is required. manifest.ts
references /icon and /apple-icon dynamic routes.

OG image + sitemap + robots — dynamic 1200×630 OG card; robots blocks
both /en/settings and /zh/settings; sitemap only emits /en routes;
JSON-LD covers SoftwareApplication + Organization + 10-Q FAQPage.

Landing page polish — Launch Dashboard button stripped of magnetic
hover + shimmer + lift + glow expansion (multiple users found it busy);
hero scramble alphabet swapped to alphanumeric (was block characters);
"15 KOL feeds" copy updated to 19 in 5 places.

PostCards source icons — new entries for btc_bottom_reversal,
funding_reversal, kol_divergence so they no longer fall through to
the generic "first letter" fallback.

Archive page filter — excludes funding_reversal + kol_divergence
(previously only excluded truth + btc_bottom_reversal so new live
signals leaked into the legacy archive).

Cache + skeleton loading — lib/cache.ts SWR module with per-key TTL,
applied across the major pages so navigation feels instant.

SignConfirmSheet — 4 hardcoded Chinese strings translated to English
(English users were seeing zh-only "需要钱包签名" etc).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-25 00:53:27 +08:00
parent 01be8e790b
commit d72323b1c6
67 changed files with 11735 additions and 2530 deletions
+277
View File
@@ -0,0 +1,277 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useLocale } from 'next-intl'
import { useAccount, useSignMessage } from 'wagmi'
import {
getTelegramStatus, tgInit, tgUnbind,
type TelegramStatus,
} from '@/lib/api'
import { signRequest } from '@/lib/signedRequest'
import { walletErrorLabel } from '@/lib/walletError'
import { confirmSign } from '@/components/wallet/SignConfirmSheet'
/**
* Settings → Telegram alerts card.
*
* Three states:
* 1. Server not configured → show "ask the admin to set up the bot"
* 2. Not bound → deep-link to bot + optional wallet-link code
* 3. Bound → status line + link to bot for preferences
*
* Preferences are managed entirely inside the bot (/trump /btc /funding /kol
* /conf /quiet). This card is just discovery + wallet-linking for Pro users.
*/
export default function TelegramCard() {
const locale = useLocale()
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 [mounted, setMounted] = useState(false)
const [status, setStatus] = useState<TelegramStatus | null>(null)
const [loading, setLoading] = useState(true)
const [err, setErr] = useState('')
const [busy, setBusy] = useState(false)
const [code, setCode] = useState<{ code: string; deep_link: string } | null>(null)
useEffect(() => { setMounted(true) }, [])
const refresh = useCallback(async () => {
if (!address) return
setLoading(true)
try {
const s = await getTelegramStatus(address.toLowerCase())
setStatus(s); setErr('')
} catch (e) {
setErr(e instanceof Error ? e.message : 'load failed')
} finally { setLoading(false) }
}, [address])
useEffect(() => { refresh() }, [refresh])
// Poll while waiting for wallet-link to complete.
useEffect(() => {
if (!code || status?.bound) return
const id = setInterval(refresh, 3000)
return () => clearInterval(id)
}, [code, status?.bound, refresh])
const botUsername = status?.bot_username ?? 'TrumpAlpha_bot'
const botLink = `https://t.me/${botUsername}`
async function handleLinkWallet() {
if (!address) return
setBusy(true); setErr('')
try {
const confirmed = await confirmSign({
label: 'Generate Telegram link code',
description: 'A one-time 6-char code valid for 10 min. Paste it into the bot to link your wallet for Pro features.',
})
if (!confirmed) { setBusy(false); return }
const env = await signRequest({
action: 'telegram_init', wallet: address, body: null, signMessageAsync,
})
const r = await tgInit(address.toLowerCase(), env)
setCode({ code: r.code, deep_link: r.deep_link })
} catch (e) {
setErr(walletErrorLabel(e, 'Cancelled'))
} finally { setBusy(false) }
}
async function handleUnbind() {
if (!address) return
setBusy(true); setErr('')
try {
const confirmed = await confirmSign({
label: 'Disconnect Telegram',
description: 'Unlinks this wallet from Telegram. Your free subscription stays active in the bot.',
})
if (!confirmed) { setBusy(false); return }
const env = await signRequest({
action: 'telegram_unbind', wallet: address, body: null, signMessageAsync,
})
await tgUnbind(address.toLowerCase(), env)
setCode(null)
await refresh()
} catch (e) {
setErr(walletErrorLabel(e, 'Cancelled'))
} finally { setBusy(false) }
}
// ── Render ───────────────────────────────────────────────────────────────
if (!mounted) return null
if (!isConnected) return null
return (
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between',
alignItems: 'center', marginBottom: 12, gap: 12, flexWrap: 'wrap' }}>
<div>
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.06em',
textTransform: 'uppercase', color: 'var(--ink-3)' }}>
{isZh ? 'Telegram 提醒' : 'Telegram alerts'}
</div>
<div style={{ fontSize: 13, color: 'var(--ink-2)', marginTop: 2 }}>
{isZh ? '高置信度信号触发时发送推送提醒' : 'Push notifications when high-conviction signals fire'}
</div>
</div>
{status?.bound && status.alerts_enabled && (
<span style={{ fontSize: 11, color: 'var(--up)', fontWeight: 600,
padding: '4px 10px', borderRadius: 6,
background: 'var(--up-soft)' }}>
{isZh ? '已开启' : 'Active'}
</span>
)}
{status?.bound && !status.alerts_enabled && (
<span style={{ fontSize: 11, color: 'var(--ink-4)', fontWeight: 600,
padding: '4px 10px', borderRadius: 6,
background: 'var(--bg-sunk)' }}>
{isZh ? '已暂停' : 'Paused'}
</span>
)}
</div>
{loading && (
<div className="skeleton sk-line sk-w-full" style={{ marginBottom: 8 }} />
)}
{/* Server not configured */}
{!loading && status && !status.configured && (
<div style={{ padding: 12, fontSize: 12, color: 'var(--ink-3)',
background: 'var(--bg-sunk)', borderRadius: 6 }}>
{isZh ? '当前服务器还没有配置 Telegram 提醒。请让管理员设置' : 'Telegram alerts are not configured on this server. Ask the operator to set'}
<code style={{ margin: '0 4px' }}>TELEGRAM_BOT_TOKEN</code> and
<code style={{ margin: '0 4px' }}>TELEGRAM_BOT_USERNAME</code>.
</div>
)}
{/* Configured — main body */}
{!loading && status?.configured && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* Bot link row — always visible */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10,
padding: '10px 14px', borderRadius: 8,
background: 'var(--bg-sunk)', border: '1px solid var(--line)' }}>
<span style={{ fontSize: 18 }}></span>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>
<a href={botLink} target="_blank" rel="noopener noreferrer"
style={{ color: 'var(--amber, #f59e0b)', textDecoration: 'none' }}>
@{botUsername}
</a>
</div>
<div style={{ fontSize: 11, color: 'var(--ink-4)', marginTop: 2 }}>
{status.bound
? (isZh ? '打开机器人调整偏好(/trump /btc /funding /kol /conf /quiet' : 'Open the bot to adjust preferences (/trump /btc /funding /kol /conf /quiet)')
: (isZh ? '打开机器人并发送 /start,无需额外账号' : 'Open the bot and send /start — no account needed')}
</div>
</div>
</div>
{/* Bound: wallet status + disconnect */}
{status.bound && (
<div style={{ fontSize: 12, color: 'var(--ink-3)',
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<span>
{status.tg_username
? <>{isZh ? '已绑定账号' : 'Linked as'} <strong style={{ color: 'var(--ink)' }}>@{status.tg_username}</strong></>
: <>{isZh ? '聊天 ID' : 'Chat'} <strong style={{ color: 'var(--ink)' }}>#{status.chat_id}</strong></>}
</span>
{status.wallet_address ? (
<>
<span style={{ color: 'var(--ink-5)' }}>·</span>
<span style={{ color: 'var(--up)', fontSize: 11 }}>Pro</span>
<span style={{ color: 'var(--ink-5)' }}>·</span>
<span>{isZh ? `已发送 ${status.total_alerts_sent ?? 0} 条提醒` : `${status.total_alerts_sent ?? 0} alerts sent`}</span>
<span style={{ color: 'var(--ink-5)' }}>·</span>
<button className="btn ghost" disabled={busy} onClick={handleUnbind}
style={{ fontSize: 11, color: 'var(--down)', padding: '2px 8px' }}>
{isZh ? '断开钱包绑定' : 'Disconnect wallet'}
</button>
</>
) : (
<>
<span style={{ color: 'var(--ink-5)' }}>·</span>
<span>{isZh ? `已发送 ${status.total_alerts_sent ?? 0} 条提醒` : `${status.total_alerts_sent ?? 0} alerts sent`}</span>
</>
)}
</div>
)}
{/* Not bound: optional wallet-link for Pro */}
{!status.bound && !code && (
<div style={{ fontSize: 12, color: 'var(--ink-4)' }}>
{isZh ? '已经在机器人里了?' : 'Already in the bot?'}{' '}
<button onClick={handleLinkWallet} disabled={busy}
style={{ background: 'none', border: 'none', cursor: 'pointer',
color: 'var(--amber, #f59e0b)', fontSize: 12, padding: 0,
textDecoration: 'underline' }}>
{isZh ? '绑定这个钱包以启用 Pro 功能' : 'Link this wallet for Pro features'}
</button>
</div>
)}
{/* Wallet-link code flow */}
{!status.bound && code && (
<WalletLinkPanel code={code.code} link={code.deep_link}
onCancel={() => setCode(null)} isZh={isZh} />
)}
</div>
)}
{err && (
<div style={{ fontSize: 11, color: 'var(--down)', marginTop: 8 }}> {err}</div>
)}
</div>
)
}
// ── Subcomponents ────────────────────────────────────────────────────────────
function WalletLinkPanel({ code, link, onCancel, isZh }: {
code: string; link: string; onCancel: () => void; isZh: boolean
}) {
const [copied, setCopied] = useState(false)
function copy() {
navigator.clipboard.writeText(code).then(() => {
setCopied(true); setTimeout(() => setCopied(false), 1500)
}).catch(() => {})
}
return (
<div style={{ padding: 14, background: 'var(--bg-sunk)',
borderRadius: 8, border: '1px solid var(--line)' }}>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8 }}>
{isZh ? '绑定钱包(Pro:' : 'Link wallet (Pro):'}
</div>
<ol style={{ paddingLeft: 18, fontSize: 12, color: 'var(--ink-2)',
lineHeight: 1.7, marginBottom: 12 }}>
<li>
{isZh ? '打开机器人:' : 'Open the bot:'}&nbsp;
<a href={link} target="_blank" rel="noopener noreferrer"
style={{ color: 'var(--amber, #f59e0b)', fontWeight: 600 }}>
{link.replace('https://', '')}
</a>
</li>
<li>
{isZh ? '发送' : 'Send'} <code style={{ background: 'var(--bg)', padding: '1px 6px',
borderRadius: 3 }}>/start {code}</code>
<button onClick={copy} style={{
marginLeft: 6, fontSize: 11, padding: '2px 8px', borderRadius: 4,
border: '1px solid var(--line)', background: 'var(--bg)',
cursor: 'pointer', color: 'var(--ink-3)',
}}>{copied ? (isZh ? '✓ 已复制' : '✓ copied') : (isZh ? '复制验证码' : 'copy code')}</button>
</li>
</ol>
<div style={{ fontSize: 11, color: 'var(--ink-4)', marginBottom: 10 }}>
{isZh ? '验证码 10 分钟内有效,绑定成功后这里会自动刷新。' : 'Code expires in 10 min. This panel updates automatically once linked.'}
</div>
<button onClick={onCancel} style={{
fontSize: 11, color: 'var(--ink-3)', background: 'none',
border: 'none', cursor: 'pointer', padding: 0,
}}>{isZh ? '取消' : 'Cancel'}</button>
</div>
)
}