Files
k 4c3c8c6f87 KOL count: 29 → 25 across marketing/SEO copy
Backend KOL_FEEDS trimmed from 29 to 25 (dead feeds removed).
Sync all hardcoded count mentions:
- layout.tsx JSON-LD, page.tsx (metric + comparison + copy)
- kol/page.tsx, KolPageClient.tsx ("and 26 more" → "and 22 more")
- glossary/page.tsx, opengraph-image.tsx
- public/llms.txt, llms-full.txt
- drop removed KOLs (Dragonfly Capital, Nic Carter) from named lists

Bundles other in-flight frontend work already in the working tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:55:27 +08:00

230 lines
8.1 KiB
TypeScript
Raw Permalink 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'
import { needsMobileWallet } from '@/lib/mobileWallet'
import MobileWalletSheet from '@/components/wallet/MobileWalletSheet'
// 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}
aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{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('')
const [mobileSheetOpen, setMobileSheetOpen] = 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])
useEffect(() => {
if (isConnected) setConnectError('')
}, [isConnected])
const locale = pathname.split('/')[1] || 'en'
const path = '/' + pathname.split('/').slice(2).join('/')
async function handleConnectWallet() {
setConnectError('')
// On mobile without an injected provider, show the wallet deep-link sheet
// instead of throwing "No wallet found". This lets users open the dApp
// inside MetaMask / Trust / Coinbase without a confusing error.
if (needsMobileWallet()) {
setMobileSheetOpen(true)
return
}
try {
const connector = await getFirstReadyConnector(connectors)
if (!connector) {
// Desktop with no extension — give a helpful hint instead of raw error.
setConnectError('No wallet extension found. Install MetaMask or another browser wallet.')
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 (
<>
<MobileWalletSheet open={mobileSheetOpen} onClose={() => setMobileSheetOpen(false)} />
<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' : ''}`}
prefetch
>
{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>
</>
)
}