feat: add Nav component
This commit is contained in:
219
app/src/components/Nav.tsx
Normal file
219
app/src/components/Nav.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user