feat: add Nav component

This commit is contained in:
thesn10
2026-06-24 23:38:32 +02:00
parent 5695df6048
commit 3cffe89620

219
app/src/components/Nav.tsx Normal file
View File

@@ -0,0 +1,219 @@
'use client'
import { usePathname, useRouter } from 'next/navigation'
import { useState, useRef, useEffect } from 'react'
import { useCluster } from '@solana/connector/react'
import { ConnectButton } from '@/components/connector/connect-button'
const NAV_LINKS = [
{ href: '/listings', label: 'Listings' },
{ href: '/escrows', label: 'Escrows' },
{ href: '/resolvers', label: 'Resolvers' },
{ href: '/dashboard', label: 'Dashboard' },
]
const NET_DOTS: Record<string, string> = {
'solana:localnet': '#FF7A59',
'solana:devnet': '#14F195',
'solana:mainnet': '#9945FF',
}
export function Nav() {
const pathname = usePathname()
const router = useRouter()
const { cluster, clusters, setCluster } = useCluster()
const [netOpen, setNetOpen] = useState(false)
const [search, setSearch] = useState('')
const netRef = useRef<HTMLDivElement>(null)
useEffect(() => {
function close(e: MouseEvent) {
if (netRef.current && !netRef.current.contains(e.target as Node)) setNetOpen(false)
}
document.addEventListener('mousedown', close)
return () => document.removeEventListener('mousedown', close)
}, [])
function handleSearch(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter' && search.trim()) {
router.push(`/search?q=${encodeURIComponent(search.trim())}`)
setSearch('')
}
}
const dot = NET_DOTS[cluster?.id ?? ''] ?? '#8A8FA3'
return (
<nav
style={{
position: 'sticky',
top: 0,
zIndex: 40,
display: 'flex',
alignItems: 'center',
gap: 18,
height: 62,
padding: '0 22px',
background: 'var(--navbg)',
backdropFilter: 'blur(14px)',
borderBottom: '1px solid var(--bd)',
}}
>
{/* Logo */}
<div
onClick={() => router.push('/listings')}
style={{ display: 'flex', alignItems: 'center', gap: 11, cursor: 'pointer', flexShrink: 0 }}
>
<div
style={{
width: 30,
height: 30,
borderRadius: 9,
background: 'linear-gradient(135deg, var(--acc), var(--acc2))',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 18px var(--accGlow)',
}}
>
<div
style={{ width: 11, height: 11, background: 'var(--bg)', transform: 'rotate(45deg)', borderRadius: 2 }}
/>
</div>
<div style={{ lineHeight: 1.05 }}>
<div style={{ fontWeight: 700, fontSize: 16, letterSpacing: '-.01em' }}>Solisting</div>
<div style={{ fontSize: 9, fontWeight: 600, letterSpacing: '.22em', color: 'var(--mut)' }}>
EXPLORER
</div>
</div>
</div>
{/* Search */}
<div style={{ position: 'relative', flex: 1, maxWidth: 440 }}>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={handleSearch}
placeholder="Search address · listing · order · escrow · resolver"
style={{
width: '100%',
height: 38,
padding: '0 14px 0 36px',
background: 'var(--bg3)',
border: '1px solid var(--bd)',
borderRadius: 10,
fontSize: 13,
color: 'var(--tx)',
outline: 'none',
}}
/>
<div
style={{
position: 'absolute',
left: 12,
top: '50%',
transform: 'translateY(-50%)',
width: 13,
height: 13,
border: '1.6px solid var(--mut)',
borderRadius: '50%',
pointerEvents: 'none',
}}
/>
</div>
{/* Nav links */}
<div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
{NAV_LINKS.map((link) => {
const active = pathname.startsWith(link.href)
return (
<button
key={link.href}
onClick={() => router.push(link.href)}
style={{
padding: '8px 13px',
borderRadius: 9,
fontSize: 13,
fontWeight: active ? 600 : 400,
color: active ? 'var(--tx)' : 'var(--mut)',
background: active ? 'var(--bg3)' : 'transparent',
}}
>
{link.label}
</button>
)
})}
</div>
{/* Network picker */}
<div ref={netRef} style={{ position: 'relative', flexShrink: 0 }}>
<button
onClick={() => setNetOpen((v) => !v)}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
height: 38,
padding: '0 12px',
borderRadius: 10,
background: 'var(--bg3)',
border: '1px solid var(--bd)',
fontSize: 13,
fontWeight: 500,
}}
>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: dot, boxShadow: `0 0 8px ${dot}` }} />
{cluster?.label ?? 'Network'}
<span style={{ color: 'var(--mut)', fontSize: 10 }}></span>
</button>
{netOpen && (
<div
style={{
position: 'absolute',
right: 0,
top: 46,
width: 188,
background: 'var(--bg2)',
border: '1px solid var(--bd)',
borderRadius: 12,
padding: 6,
boxShadow: '0 18px 44px rgba(0,0,0,.5)',
zIndex: 50,
}}
>
{clusters.map((c) => {
const d = NET_DOTS[c.id] ?? '#8A8FA3'
const selected = c.id === cluster?.id
return (
<button
key={c.id}
onClick={() => { setCluster(c.id as never); setNetOpen(false) }}
style={{
display: 'flex',
alignItems: 'center',
gap: 9,
width: '100%',
padding: '9px 10px',
borderRadius: 8,
fontSize: 13,
textAlign: 'left',
background: selected ? 'var(--bg3)' : 'transparent',
}}
>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: d }} />
<span style={{ flex: 1 }}>{c.label}</span>
{selected && <span style={{ color: 'var(--acc2)', fontSize: 12 }}></span>}
</button>
)
})}
</div>
)}
</div>
{/* Wallet */}
<div style={{ flexShrink: 0 }}>
<ConnectButton />
</div>
</nav>
)
}