File size: 9,615 Bytes
bbff783 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
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>
);
} |