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

205 lines
5.8 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 React from 'react'
interface PaginationProps {
page: number
total: number // total pages
count: number // total items
pageSize: number
onChange: (page: number) => void
/** Scroll to top on page change. Default true. */
scrollTop?: boolean
}
/**
* Shared pagination control used across all list pages.
*
* Visual design:
* 1 2 … 5 [6] 7 … 12 Showing 151180 of 340
*
* Active page uses brand orange. Ellipsis collapses distant pages.
* Prev/Next disabled at boundaries (dimmed, not hidden).
*/
export default function Pagination({
page, total, count, pageSize, onChange, scrollTop = true,
}: PaginationProps) {
if (total <= 1) return null
const from = (page - 1) * pageSize + 1
const to = Math.min(page * pageSize, count)
function go(p: number) {
if (p < 1 || p > total || p === page) return
onChange(p)
if (scrollTop) window.scrollTo({ top: 0, behavior: 'smooth' })
}
/** Page number list with ellipsis compression. */
function pages(): (number | '…')[] {
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1)
const out: (number | '…')[] = []
const push = (n: number | '…') => {
if (out[out.length - 1] !== n) out.push(n)
}
push(1)
if (page > 3) push('…')
for (let i = Math.max(2, page - 1); i <= Math.min(total - 1, page + 1); i++) push(i)
if (page < total - 2) push('…')
push(total)
return out
}
return (
<div style={wrapStyle}>
{/* ← Prev */}
<NavBtn onClick={() => go(page - 1)} disabled={page === 1} label="Previous page">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M9 11L5 7l4-4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span>Prev</span>
</NavBtn>
{/* Page numbers */}
<div style={numsStyle}>
{pages().map((n, i) =>
n === '…' ? (
<span key={`e${i}`} style={ellipsisStyle}></span>
) : (
<PageBtn key={n} num={n as number} active={n === page} onClick={go} />
)
)}
</div>
{/* Next → */}
<NavBtn onClick={() => go(page + 1)} disabled={page === total} label="Next page">
<span>Next</span>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M5 3l4 4-4 4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</NavBtn>
{/* Count */}
<span style={countStyle}>
{from}{to} <span style={{ opacity: 0.5 }}>of</span> {count.toLocaleString()}
</span>
</div>
)
}
// ── Sub-components ────────────────────────────────────────────────────────────
function PageBtn({ num, active, onClick }: { num: number; active: boolean; onClick: (n: number) => void }) {
const [hover, setHover] = React.useState(false)
const bg = active
? 'var(--amber)'
: hover
? 'var(--bg-sunk)'
: 'transparent'
const color = active
? '#fff'
: hover
? 'var(--ink)'
: 'var(--ink-2)'
const border = active
? '1.5px solid var(--amber)'
: '1.5px solid var(--line)'
return (
<button
onClick={() => onClick(num)}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
aria-current={active ? 'page' : undefined}
style={{
width: 34, height: 34,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
borderRadius: 8,
border,
background: bg,
color,
fontSize: 13,
fontWeight: active ? 700 : 400,
cursor: 'pointer',
transition: 'background 0.12s, color 0.12s, border-color 0.12s',
boxShadow: active ? '0 1px 4px rgba(245,165,36,0.25)' : 'none',
flexShrink: 0,
}}
>
{num}
</button>
)
}
function NavBtn({
onClick, disabled, label, children,
}: {
onClick: () => void; disabled: boolean; label: string; children: React.ReactNode
}) {
const [hover, setHover] = React.useState(false)
return (
<button
onClick={onClick}
disabled={disabled}
aria-label={label}
onMouseEnter={() => !disabled && setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
height: 34, padding: '0 12px',
borderRadius: 8,
border: '1.5px solid var(--line)',
background: hover && !disabled ? 'var(--bg-sunk)' : 'transparent',
color: disabled ? 'var(--ink-4)' : hover ? 'var(--ink)' : 'var(--ink-2)',
fontSize: 12,
fontWeight: 500,
cursor: disabled ? 'not-allowed' : 'pointer',
transition: 'background 0.12s, color 0.12s',
flexShrink: 0,
}}
>
{children}
</button>
)
}
// ── Styles ────────────────────────────────────────────────────────────────────
const wrapStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
padding: '28px 0 8px',
flexWrap: 'wrap',
}
const numsStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: 4,
flexWrap: 'wrap',
justifyContent: 'center',
}
const ellipsisStyle: React.CSSProperties = {
width: 28,
textAlign: 'center',
fontSize: 13,
color: 'var(--ink-4)',
userSelect: 'none',
flexShrink: 0,
}
const countStyle: React.CSSProperties = {
fontSize: 11,
color: 'var(--ink-3)',
fontVariantNumeric: 'tabular-nums',
marginLeft: 6,
whiteSpace: 'nowrap',
}