Files
trumpsignal-frontend/components/nav/Navbar.tsx
T
k f34ae9eb00 feat(macro-vibes): rename BTC Signal → Macro Vibes; add MacroPanel UI
Module rename across page H1, navbar tab, URL (/en/btc → /en/macro),
all metadata/JSON-LD, sitemap, llms.txt, opengraph image, and SystemControl
copy. Old /btc route fully removed.

New components/btc/MacroPanel.tsx polls /api/macro/snapshot and lays out
the 8 indicators in four sections (Valuation / Bottom trigger reference /
Market structure / Sentiment & flows / Positioning) with tone-coloured
values, current-band threshold chips, and a single CoinGlass / source
chart link per card. Composite -100..+100 needle pulses on score change.

Also fixes the pinned bottom-reversal alert on the homepage, which still
linked to the now-404 /en/btc — now routes to /en/macro.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 01:05:18 +08:00

187 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { useTranslations } from 'next-intl'
// i18n shelved — LanguageSwitch hidden but kept on disk for future revival.
// import LanguageSwitch from './LanguageSwitch'
function BrandMark() {
return <span className="brand-mark">α</span>
}
function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
useEffect(() => {
const stored = localStorage.getItem('theme') as 'light' | 'dark' | null
const t = stored || 'light'
setTheme(t)
document.documentElement.dataset.theme = t
}, [])
function toggle() {
const next = theme === 'dark' ? 'light' : 'dark'
setTheme(next)
localStorage.setItem('theme', next)
document.documentElement.dataset.theme = next
}
return (
<button className="icon-btn theme-toggle" onClick={toggle}>
{theme === 'dark' ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="2" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
</svg>
)}
</button>
)
}
export default function Navbar() {
const t = useTranslations('nav')
const pathname = usePathname()
const { address, isConnected } = useAccount()
const { connect, connectors } = useConnect()
const { disconnect } = useDisconnect()
const [mounted, setMounted] = useState(false)
const [walletMenuOpen, setWalletMenuOpen] = useState(false)
const [copied, setCopied] = useState(false)
async function copyAddress(addr: string) {
let ok = false
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(addr)
ok = true
} else {
// Fallback for non-secure contexts where the Clipboard API is absent.
const ta = document.createElement('textarea')
ta.value = addr
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
ok = document.execCommand('copy')
document.body.removeChild(ta)
}
} catch { ok = false }
setCopied(ok)
setTimeout(() => setCopied(false), 1500)
}
useEffect(() => { setMounted(true) }, [])
useEffect(() => { setWalletMenuOpen(false) }, [pathname, address])
const locale = pathname.split('/')[1] || 'en'
const path = '/' + pathname.split('/').slice(2).join('/')
// Two independent trading systems get their own top-level tabs — no
// intermediate "Signals" hub. Trump = event scalp; BTC = reversal.
const tabs = [
{ key: '/', label: t('tabs.overview') },
{ key: '/trump', label: t('tabs.trump') },
{ key: '/macro', label: t('tabs.vibes') },
{ key: '/kol', label: t('tabs.kol') },
{ key: '/trades', label: t('tabs.trades') },
{ key: '/analytics', label: t('tabs.analytics') },
{ key: '/settings', label: t('tabs.settings') },
]
function isActive(key: string) {
if (key === '/') return path === '/' || path === '//'
// /posts (legacy hub) still resolves but isn't a tab; treat it + /archive
// as part of the Trump section so a tab stays highlighted there.
if (key === '/trump') return path.startsWith('/trump') || path.startsWith('/posts') || path.startsWith('/archive')
return path.startsWith(key)
}
const shortAddr = address ? `${address.slice(0, 6)}${address.slice(-4)}` : null
return (
<nav className="nav">
<Link href={`/${locale}`} className="brand" style={{ textDecoration: 'none', color: 'inherit' }}>
<BrandMark />
<span>{t('brand')}</span>
</Link>
<div className="nav-tabs">
{tabs.map(tab => (
<Link
key={tab.key}
href={`/${locale}${tab.key === '/' ? '' : tab.key}`}
className={`nav-tab ${isActive(tab.key) ? 'active' : ''}`}
>
{tab.label}
</Link>
))}
</div>
<div className="nav-spacer" />
<div className="nav-right">
{/* <LanguageSwitch /> — shelved until i18n coverage is complete */}
<ThemeToggle />
{!mounted ? (
<button className="connect-btn lg" suppressHydrationWarning>{t('actions.connectWallet')}</button>
) : isConnected && shortAddr ? (
<div className="wallet-menu-wrap" style={{ position: 'relative' }}>
<button
className="wallet-chip"
onClick={() => setWalletMenuOpen(open => !open)}
onBlur={(e) => {
if (!e.currentTarget.parentElement?.contains(e.relatedTarget as Node | null)) {
setWalletMenuOpen(false)
}
}}
aria-expanded={walletMenuOpen}
aria-haspopup="menu"
>
<span className="ava" />
<span className="mono">{shortAddr}</span>
</button>
<div className={`wallet-menu ${walletMenuOpen ? 'open' : ''}`} role="menu">
<button
role="menuitem"
onMouseDown={(e) => {
e.preventDefault()
if (address) copyAddress(address)
}}
>
{copied ? t('actions.copied') : t('actions.copyAddress')}
</button>
<button
role="menuitem"
className="danger"
onMouseDown={(e) => {
e.preventDefault()
disconnect()
setWalletMenuOpen(false)
}}
>
{t('actions.disconnect')}
</button>
</div>
</div>
) : (
<button
className="connect-btn lg"
onClick={() => {
const connector = connectors[0]
if (connector) connect({ connector })
}}
>
{t('actions.connectWallet')}
</button>
)}
</div>
</nav>
)
}