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>
208 lines
7.2 KiB
TypeScript
208 lines
7.2 KiB
TypeScript
'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>
|
||
)
|
||
}
|