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>
This commit is contained in:
@@ -6,6 +6,8 @@ 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'
|
||||
|
||||
@@ -31,7 +33,12 @@ function ThemeToggle() {
|
||||
}
|
||||
|
||||
return (
|
||||
<button className="icon-btn theme-toggle" onClick={toggle}>
|
||||
<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" />
|
||||
@@ -56,6 +63,7 @@ export default function Navbar() {
|
||||
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
|
||||
@@ -89,10 +97,20 @@ export default function Navbar() {
|
||||
|
||||
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) {
|
||||
setConnectError('No wallet connector is available right now.')
|
||||
// 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 })
|
||||
@@ -124,6 +142,8 @@ export default function Navbar() {
|
||||
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 />
|
||||
@@ -204,5 +224,6 @@ export default function Navbar() {
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Ticker — horizontal scrolling price tape under the navbar, the way a crypto
|
||||
* terminal shows a running market strip. Driven by the shared WebSocket price
|
||||
* feed (no new socket). Each asset cell flashes green/red on a price change and
|
||||
* shows the live last price.
|
||||
*
|
||||
* Behaviour notes:
|
||||
* - The strip is duplicated once and translated -50% in a CSS marquee so the
|
||||
* loop is seamless (`ticker-marquee` keyframe in globals.css).
|
||||
* - prefers-reduced-motion disables the scroll (CSS), so the tape becomes a
|
||||
* static, readable row instead of moving.
|
||||
* - The animation is paused on hover so a user can read a specific price.
|
||||
* - Assets with no tick yet render "—" rather than a stale zero.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { useWsSubscribe } from '@/lib/wsContext'
|
||||
|
||||
// Assets the backend streams (see binance ASSET_MAP + hl_price_feed). Order is
|
||||
// the display order in the tape.
|
||||
const ASSETS = ['BTC', 'ETH', 'SOL', 'BNB', 'DOGE', 'LINK', 'AAVE', 'TRUMP', 'HYPE'] as const
|
||||
|
||||
interface Cell {
|
||||
price: number | null
|
||||
dir: 'up' | 'down' | null
|
||||
}
|
||||
|
||||
function fmtPrice(p: number): string {
|
||||
if (p >= 1000) return '$' + p.toLocaleString('en-US', { maximumFractionDigits: 0 })
|
||||
if (p >= 1) return '$' + p.toLocaleString('en-US', { maximumFractionDigits: 2 })
|
||||
return '$' + p.toLocaleString('en-US', { maximumFractionDigits: 4 })
|
||||
}
|
||||
|
||||
export default function Ticker() {
|
||||
const [cells, setCells] = useState<Record<string, Cell>>({})
|
||||
// Per-asset flash-clear timers so a fast stream doesn't leak setTimeouts.
|
||||
const timers = useRef<Record<string, ReturnType<typeof setTimeout>>>({})
|
||||
|
||||
useWsSubscribe('price', (msg) => {
|
||||
const m = msg as { asset?: string; price?: number }
|
||||
if (!m.asset || typeof m.price !== 'number') return
|
||||
const asset = m.asset.toUpperCase()
|
||||
if (!ASSETS.includes(asset as (typeof ASSETS)[number])) return
|
||||
|
||||
setCells((prev) => {
|
||||
const old = prev[asset]
|
||||
const dir: Cell['dir'] =
|
||||
old?.price != null && m.price !== old.price
|
||||
? (m.price! > old.price ? 'up' : 'down')
|
||||
: old?.dir ?? null
|
||||
return { ...prev, [asset]: { price: m.price!, dir } }
|
||||
})
|
||||
|
||||
// Clear the flash after 600ms.
|
||||
clearTimeout(timers.current[asset])
|
||||
timers.current[asset] = setTimeout(() => {
|
||||
setCells((prev) => (prev[asset] ? { ...prev, [asset]: { ...prev[asset], dir: null } } : prev))
|
||||
}, 600)
|
||||
})
|
||||
|
||||
const strip = ASSETS.map((a) => {
|
||||
const c = cells[a]
|
||||
return (
|
||||
<span key={a} className={`ticker-cell ${c?.dir ? `tick-flash-${c.dir}` : ''}`}>
|
||||
<span className="ticker-sym">{a}</span>
|
||||
<span className="ticker-px">{c?.price != null ? fmtPrice(c.price) : '—'}</span>
|
||||
<span className={`ticker-arrow ${c?.dir ?? ''}`} aria-hidden>
|
||||
{c?.dir === 'up' ? '▲' : c?.dir === 'down' ? '▼' : ''}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="ticker" role="region" aria-label="Live market prices">
|
||||
<div className="ticker-track">
|
||||
{strip}
|
||||
{/* duplicate for seamless loop */}
|
||||
<span aria-hidden style={{ display: 'contents' }}>{strip}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user