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:
k
2026-06-09 22:55:27 +08:00
parent 9e0f6554cb
commit 4c3c8c6f87
57 changed files with 3464 additions and 1855 deletions
+85
View File
@@ -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>
)
}