Files
trumpsignal-frontend/components/nav/Navbar.tsx
T
k d01adc4790 style(dashboard): unify Macro composite into one card + dark-mode fix
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>
2026-05-27 11:25:59 +08:00

208 lines
7.2 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'
import { getFirstReadyConnector, walletConnectErrorLabel } from '@/lib/walletConnect'
// 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 { connectAsync, connectors } = useConnect()
const { disconnect } = useDisconnect()
const [mounted, setMounted] = useState(false)
const [walletMenuOpen, setWalletMenuOpen] = useState(false)
const [copied, setCopied] = useState(false)
const [connectError, setConnectError] = useState('')
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])
useEffect(() => {
if (isConnected) setConnectError('')
}, [isConnected])
const locale = pathname.split('/')[1] || 'en'
const path = '/' + pathname.split('/').slice(2).join('/')
async function handleConnectWallet() {
setConnectError('')
try {
const connector = await getFirstReadyConnector(connectors)
if (!connector) {
setConnectError('No wallet connector is available right now.')
return
}
await connectAsync({ connector })
} catch (err: unknown) {
setConnectError(walletConnectErrorLabel(err))
}
}
// 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 />
<div className="wallet-anchor">
{!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={() => { void handleConnectWallet() }}
>
{t('actions.connectWallet')}
</button>
)}
{!isConnected && connectError ? (
<p className="wallet-connect-error" role="alert">{connectError}</p>
) : null}
</div>
</div>
</nav>
)
}