|
|
import { useState, useEffect, useCallback, useMemo } from 'react'; |
|
|
import { motion, AnimatePresence } from 'framer-motion'; |
|
|
import { FiMenu, FiX, FiExternalLink, FiArrowUpRight } from 'react-icons/fi'; |
|
|
import { HiSparkles } from 'react-icons/hi'; |
|
|
|
|
|
export default function Navbar() { |
|
|
const [isOpen, setIsOpen] = useState(false); |
|
|
const [scrolled, setScrolled] = useState(false); |
|
|
|
|
|
|
|
|
const menuItems = useMemo( |
|
|
() => [ |
|
|
{ label: 'Features', href: '#features' }, |
|
|
{ label: 'Converters', href: '#converters' }, |
|
|
{ label: 'Social Media', href: '#social-media' }, |
|
|
{ label: 'About', href: '#about' }, |
|
|
], |
|
|
[], |
|
|
); |
|
|
|
|
|
|
|
|
const handleScroll = useCallback(() => { |
|
|
const scrollTop = window.scrollY; |
|
|
setScrolled(scrollTop > 20); |
|
|
}, []); |
|
|
|
|
|
useEffect(() => { |
|
|
let ticking = false; |
|
|
|
|
|
const throttledScroll = () => { |
|
|
if (!ticking) { |
|
|
requestAnimationFrame(() => { |
|
|
handleScroll(); |
|
|
ticking = false; |
|
|
}); |
|
|
ticking = true; |
|
|
} |
|
|
}; |
|
|
|
|
|
window.addEventListener('scroll', throttledScroll, { passive: true }); |
|
|
return () => window.removeEventListener('scroll', throttledScroll); |
|
|
}, [handleScroll]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const handleClickOutside = event => { |
|
|
if (isOpen && !event.target.closest('nav')) { |
|
|
setIsOpen(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
document.addEventListener('click', handleClickOutside); |
|
|
return () => document.removeEventListener('click', handleClickOutside); |
|
|
}, [isOpen]); |
|
|
|
|
|
|
|
|
const easeInOutCubic = (t) => { |
|
|
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; |
|
|
}; |
|
|
|
|
|
const smoothScrollTo = useCallback((targetPosition, duration = 1200) => { |
|
|
const startPosition = window.pageYOffset; |
|
|
const distance = targetPosition - startPosition; |
|
|
let startTime = null; |
|
|
|
|
|
const animation = (currentTime) => { |
|
|
if (startTime === null) startTime = currentTime; |
|
|
const timeElapsed = currentTime - startTime; |
|
|
const progress = Math.min(timeElapsed / duration, 1); |
|
|
const ease = easeInOutCubic(progress); |
|
|
|
|
|
window.scrollTo(0, startPosition + distance * ease); |
|
|
|
|
|
if (timeElapsed < duration) { |
|
|
requestAnimationFrame(animation); |
|
|
} |
|
|
}; |
|
|
|
|
|
requestAnimationFrame(animation); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const handleSmoothScroll = useCallback( |
|
|
href => { |
|
|
if (href.startsWith('#')) { |
|
|
const element = document.querySelector(href); |
|
|
if (element) { |
|
|
const offset = 80; |
|
|
const elementPosition = element.getBoundingClientRect().top; |
|
|
const offsetPosition = elementPosition + window.pageYOffset - offset; |
|
|
|
|
|
smoothScrollTo(offsetPosition, 1200); |
|
|
setIsOpen(false); |
|
|
} |
|
|
} |
|
|
}, |
|
|
[smoothScrollTo, setIsOpen], |
|
|
); |
|
|
|
|
|
|
|
|
const scrollToTop = useCallback(() => { |
|
|
smoothScrollTo(0, 1000); |
|
|
}, [smoothScrollTo]); |
|
|
|
|
|
return ( |
|
|
<motion.nav |
|
|
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ease-out ${ |
|
|
scrolled |
|
|
? 'bg-[var(--background-secondary)]/95 backdrop-blur-xl border-b border-[var(--border)] shadow-lg' |
|
|
: 'bg-transparent' |
|
|
}`} |
|
|
initial={{ y: -100, opacity: 0 }} |
|
|
animate={{ y: 0, opacity: 1 }} |
|
|
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }} |
|
|
> |
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
|
|
<div className="flex items-center justify-between h-14 sm:h-16"> |
|
|
{/* Logo */} |
|
|
<motion.div |
|
|
className="flex items-center space-x-2 cursor-pointer group" |
|
|
whileHover={{ scale: 1.02 }} |
|
|
whileTap={{ scale: 0.98 }} |
|
|
transition={{ duration: 0.2 }} |
|
|
onClick={scrollToTop} |
|
|
> |
|
|
<div className="relative"> |
|
|
<HiSparkles className="text-xl sm:text-2xl text-[var(--accent)] group-hover:text-[var(--accent-hover)] transition-colors duration-300" /> |
|
|
<div className="absolute inset-0 bg-[var(--accent)] blur-lg opacity-20 group-hover:opacity-30 transition-opacity duration-300"></div> |
|
|
</div> |
|
|
<span className="text-lg sm:text-xl font-bold gradient-text"> |
|
|
LumaKit |
|
|
</span> |
|
|
</motion.div> |
|
|
|
|
|
{/* Desktop Menu */} |
|
|
<div className="hidden md:flex items-center space-x-6 lg:space-x-8"> |
|
|
{menuItems.map((item, index) => ( |
|
|
<motion.button |
|
|
key={index} |
|
|
onClick={() => handleSmoothScroll(item.href)} |
|
|
className="text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors duration-300 text-sm font-medium relative group" |
|
|
whileHover={{ y: -1 }} |
|
|
transition={{ duration: 0.2 }} |
|
|
> |
|
|
{item.label} |
|
|
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[var(--accent)] group-hover:w-full transition-all duration-300"></span> |
|
|
</motion.button> |
|
|
))} |
|
|
<motion.a |
|
|
onClick={() => handleSmoothScroll("#features")} |
|
|
rel="noopener noreferrer" |
|
|
className="group relative flex items-center space-x-2 bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-[var(--background)] px-4 py-2 rounded-xl transition-all duration-300 text-sm font-medium shadow-lg hover:shadow-xl overflow-hidden" |
|
|
whileHover={{ scale: 1.02, y: -1 }} |
|
|
whileTap={{ scale: 0.98 }} |
|
|
transition={{ duration: 0.2 }} |
|
|
> |
|
|
<div className="absolute inset-0 bg-gradient-to-r from-[var(--accent)] to-[var(--accent-hover)] opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> |
|
|
<FiExternalLink className="text-sm relative z-10" /> |
|
|
<span className="relative z-10">Try LumaKit</span> |
|
|
</motion.a> |
|
|
</div> |
|
|
|
|
|
{/* Mobile Menu Button */} |
|
|
<motion.button |
|
|
className="md:hidden text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors duration-300 p-2 rounded-lg hover:bg-[var(--background-secondary)]" |
|
|
onClick={() => setIsOpen(!isOpen)} |
|
|
whileTap={{ scale: 0.95 }} |
|
|
aria-label="Toggle menu" |
|
|
> |
|
|
<motion.div |
|
|
animate={isOpen ? 'open' : 'closed'} |
|
|
variants={{ |
|
|
open: { rotate: 180 }, |
|
|
closed: { rotate: 0 }, |
|
|
}} |
|
|
transition={{ duration: 0.3 }} |
|
|
> |
|
|
{isOpen ? <FiX size={20} /> : <FiMenu size={20} />} |
|
|
</motion.div> |
|
|
</motion.button> |
|
|
</div> |
|
|
|
|
|
{/* Mobile Menu */} |
|
|
<AnimatePresence mode="wait"> |
|
|
{isOpen && ( |
|
|
<motion.div |
|
|
className="md:hidden absolute top-full left-0 right-0 bg-[var(--background-secondary)]/98 backdrop-blur-xl border-b border-[var(--border)] shadow-2xl" |
|
|
initial={{ opacity: 0, height: 0, y: -10 }} |
|
|
animate={{ opacity: 1, height: 'auto', y: 0 }} |
|
|
exit={{ opacity: 0, height: 0, y: -10 }} |
|
|
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }} |
|
|
> |
|
|
<div className="px-4 py-6 space-y-4"> |
|
|
{menuItems.map((item, index) => ( |
|
|
<motion.button |
|
|
key={index} |
|
|
onClick={() => handleSmoothScroll(item.href)} |
|
|
className="block w-full text-left text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors duration-300 text-base font-medium py-2 px-3 rounded-lg hover:bg-[var(--background-tertiary)]" |
|
|
initial={{ opacity: 0, x: -20 }} |
|
|
animate={{ opacity: 1, x: 0 }} |
|
|
transition={{ delay: index * 0.1, duration: 0.3 }} |
|
|
> |
|
|
{item.label} |
|
|
</motion.button> |
|
|
))} |
|
|
<motion.a |
|
|
href="https://huggingface.co/spaces/YoruAkio/LumaKit" |
|
|
target="_blank" |
|
|
rel="noopener noreferrer" |
|
|
className="group relative flex items-center space-x-2 bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-[var(--background)] px-4 py-3 rounded-xl transition-all duration-300 text-base font-medium w-fit shadow-lg overflow-hidden" |
|
|
onClick={() => setIsOpen(false)} |
|
|
initial={{ opacity: 0, x: -20 }} |
|
|
animate={{ opacity: 1, x: 0 }} |
|
|
transition={{ delay: 0.4, duration: 0.3 }} |
|
|
> |
|
|
<div className="absolute inset-0 bg-gradient-to-r from-[var(--accent)] to-[var(--accent-hover)] opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> |
|
|
<FiExternalLink className="text-sm relative z-10" /> |
|
|
<span className="relative z-10">Try LumaKit</span> |
|
|
<FiArrowUpRight className="text-sm group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-200 relative z-10" /> |
|
|
</motion.a> |
|
|
</div> |
|
|
</motion.div> |
|
|
)} |
|
|
</AnimatePresence> |
|
|
</div> |
|
|
</motion.nav> |
|
|
); |
|
|
} |