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:
@@ -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:'}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user