d01adc4790
for Performance accent
User flagged the macro index box on the overview page as feeling
disjointed. Reshaped it from three stacked elements (band → track →
scale) into a single bordered card so it reads as ONE component.
- JSX: wrap the three pieces in <div className="overview-macro-card">
- CSS: new .overview-macro-card with tone-coloured rim (bull/bear/neutral)
- Solid neutral-gray filled needle (was a hollow ring — looked like a
placeholder); tone-coloured background + white inner ring + double
shadow so it stands out on any gradient position
- Removed the .overview-score-fill overlay — the gradient already
encodes the spectrum; layering an opaque fill obscured it near 0
- Thinner track (14px vs 22px), tighter scale labels, smaller pill
- Added "TODAY · 8 INDICATORS" stamp next to the title — gives users
a quick anchor of what they're looking at + freshness
Plus: dark-mode override for .overview-stat-card.accent (the Performance
card). It was using a cream gradient that floated as a glaring
out-of-theme block on dark mode. Mirrored the existing .kpi.accent dark
treatment so it stays visually grouped with the rest of the dashboard.
Also includes the in-flight overview rewrite from the other AI tool
(legacy /btc redirect to /macro, middleware.ts replacing proxy.ts for
Next.js routing, refactored several dashboard panels). TypeScript clean,
production build passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
293 lines
13 KiB
TypeScript
293 lines
13 KiB
TypeScript
'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) {
|
||
setLoading(false)
|
||
setStatus(null)
|
||
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 (
|
||
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
|
||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 6 }}>
|
||
Telegram alerts
|
||
</div>
|
||
<div style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.6 }}>
|
||
Connect your wallet first. After that, you can link Telegram for alert delivery and Pro wallet-bound notifications.
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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>
|
||
)
|
||
}
|