YoruAkio's picture
feat: Add responsive Navbar component with smooth scrolling and enhanced UX
bbff783
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);
// Memoized menu items for better performance
const menuItems = useMemo(
() => [
{ label: 'Features', href: '#features' },
{ label: 'Converters', href: '#converters' },
{ label: 'Social Media', href: '#social-media' },
{ label: 'About', href: '#about' },
],
[],
);
// Optimized scroll handler with throttling
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]);
// Close mobile menu when clicking outside
useEffect(() => {
const handleClickOutside = event => {
if (isOpen && !event.target.closest('nav')) {
setIsOpen(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, [isOpen]);
// Enhanced smooth scroll with easing function
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);
}, []);
// Smooth scroll handler with improved offset calculation and animation
const handleSmoothScroll = useCallback(
href => {
if (href.startsWith('#')) {
const element = document.querySelector(href);
if (element) {
const offset = 80; // Account for fixed navbar height
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
smoothScrollTo(offsetPosition, 1200); // 1.2 second duration
setIsOpen(false);
}
}
},
[smoothScrollTo, setIsOpen],
);
// Enhanced scroll to top with animation
const scrollToTop = useCallback(() => {
smoothScrollTo(0, 1000); // 1 second duration for scroll to top
}, [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>
);
}