first commit
This commit is contained in:
166
src/components/layout/Navbar.tsx
Normal file
166
src/components/layout/Navbar.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
import { NearleLogo } from '@/components/ui/NearleLogo'
|
||||
import { useScrollSpy } from '@/lib/useScrollSpy'
|
||||
|
||||
type NavItem = {
|
||||
label: string
|
||||
href: string
|
||||
sectionId?: string
|
||||
}
|
||||
|
||||
const HOME_SECTIONS = [
|
||||
'home',
|
||||
'features',
|
||||
'map',
|
||||
'download',
|
||||
'contact',
|
||||
]
|
||||
|
||||
const NAV: NavItem[] = [
|
||||
{ label: 'Home', href: '/', sectionId: 'home' },
|
||||
{ label: 'About', href: '/about' },
|
||||
{ label: 'Communities', href: '/communities', sectionId: 'map' },
|
||||
{ label: 'Businesses', href: '/businesses' },
|
||||
{ label: 'Download', href: '/#download', sectionId: 'download' },
|
||||
{ label: 'Contact', href: '/contact', sectionId: 'contact' },
|
||||
]
|
||||
|
||||
export function Navbar() {
|
||||
const pathname = usePathname()
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const activeSection = useScrollSpy(HOME_SECTIONS)
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 60)
|
||||
onScroll()
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
return () => window.removeEventListener('scroll', onScroll)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false)
|
||||
}, [pathname])
|
||||
|
||||
const isActive = (item: NavItem) => {
|
||||
if (item.href.startsWith('/#')) {
|
||||
if (pathname !== '/') return false
|
||||
return activeSection === item.sectionId
|
||||
}
|
||||
if (item.href === '/') {
|
||||
return (
|
||||
pathname === '/' &&
|
||||
(activeSection === 'home' || activeSection === null)
|
||||
)
|
||||
}
|
||||
return pathname === item.href || pathname.startsWith(item.href + '/')
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 inset-x-0 z-50 transition-all duration-400 ${
|
||||
scrolled
|
||||
? 'bg-white/85 backdrop-blur-xl border-b border-purple-lavender shadow-nearle-sm'
|
||||
: 'bg-transparent border-b border-transparent'
|
||||
}`}
|
||||
>
|
||||
<nav className="max-w-7xl mx-auto px-5 sm:px-8 h-14 md:h-16 flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2 group">
|
||||
<NearleLogo size={32} />
|
||||
<span className="font-display font-extrabold text-purple-deep text-xl tracking-tight">
|
||||
Nearle
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<ul className="hidden lg:flex items-center gap-1">
|
||||
{NAV.map((item) => {
|
||||
const active = isActive(item)
|
||||
return (
|
||||
<li key={item.label}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`px-4 py-1.5 rounded-full text-sm transition-all duration-200 ${
|
||||
active
|
||||
? 'bg-purple-lavender text-purple-deep font-semibold'
|
||||
: 'text-nearle-mid hover:text-purple-deep'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<div className="hidden lg:flex items-center gap-3">
|
||||
<Link
|
||||
href="/#download"
|
||||
className="bg-purple-deep text-white rounded-full px-5 py-2 font-semibold text-sm hover:bg-purple-primary shadow-cta hover:shadow-cta-hover transition-all"
|
||||
>
|
||||
Get Ready Now
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="Toggle menu"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="lg:hidden inline-flex items-center justify-center w-10 h-10 rounded-full text-purple-deep hover:bg-purple-soft transition"
|
||||
>
|
||||
{open ? <X size={22} /> : <Menu size={22} />}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
key="mobile-drawer"
|
||||
initial={{ opacity: 0, y: -12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="lg:hidden bg-white border-b border-purple-lavender shadow-nearle-sm"
|
||||
>
|
||||
<ul className="max-w-7xl mx-auto px-5 py-4 flex flex-col gap-1">
|
||||
{NAV.map((item, i) => {
|
||||
const active = isActive(item)
|
||||
return (
|
||||
<motion.li
|
||||
key={item.label}
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.04 }}
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`block px-4 py-3 rounded-xl text-base transition-all ${
|
||||
active
|
||||
? 'bg-purple-lavender text-purple-deep font-semibold'
|
||||
: 'text-nearle-mid hover:bg-purple-soft hover:text-purple-deep'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</motion.li>
|
||||
)
|
||||
})}
|
||||
<li className="pt-3">
|
||||
<Link
|
||||
href="/#download"
|
||||
className="block text-center bg-purple-deep text-white rounded-full px-5 py-3 font-semibold hover:bg-purple-primary shadow-cta transition-all"
|
||||
>
|
||||
Get Ready Now
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user