Files
carrotskin/src/components/Navbar.tsx

388 lines
16 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import { useRouter, usePathname } from 'next/navigation';
import { Bars3Icon, XMarkIcon, UserCircleIcon } from '@heroicons/react/24/outline';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '@/contexts/AuthContext';
export default function Navbar() {
const [isOpen, setIsOpen] = useState(false);
const [isHidden, setIsHidden] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const [showScrollTop, setShowScrollTop] = useState(false);
const [navbarHeight, setNavbarHeight] = useState(0);
const navbarRef = useRef<HTMLElement>(null);
const { user, isAuthenticated, logout } = useAuth();
const router = useRouter();
const pathname = usePathname();
// 在auth页面隐藏navbar
const isAuthPage = pathname === '/auth';
// 检测navbar高度并设置CSS自定义属性
useEffect(() => {
const updateHeight = () => {
if (navbarRef.current) {
const height = navbarRef.current.offsetHeight;
setNavbarHeight(height);
document.documentElement.style.setProperty('--navbar-height', `${height}px`);
}
};
updateHeight();
window.addEventListener('resize', updateHeight);
return () => window.removeEventListener('resize', updateHeight);
}, []);
useEffect(() => {
let lastScrollY = 0;
let ticking = false;
const handleScroll = () => {
if (!ticking) {
window.requestAnimationFrame(() => {
const currentScrollY = window.scrollY;
// 检测是否滚动到顶部
setIsScrolled(currentScrollY > 20);
// 显示返回顶部按钮滚动超过300px
setShowScrollTop(currentScrollY > 300);
// 更敏感的隐藏逻辑:只要往下滚动就隐藏,不管滚动多少
if (!isAuthPage && currentScrollY > lastScrollY && currentScrollY > 10) {
setIsHidden(true);
} else if (currentScrollY < lastScrollY) { // 往上滚动就显示
setIsHidden(false);
}
lastScrollY = currentScrollY;
ticking = false;
});
ticking = true;
}
};
if (!isAuthPage) {
window.addEventListener('scroll', handleScroll, { passive: true });
}
return () => window.removeEventListener('scroll', handleScroll);
}, [isAuthPage]);
const handleLogout = () => {
logout();
router.push('/');
setIsOpen(false);
};
const handleLinkClick = () => {
setIsOpen(false);
};
// 在auth页面不渲染navbar
if (isAuthPage) {
return null;
}
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
};
return (
<motion.nav
ref={navbarRef}
initial={{ y: 0 }}
animate={{ y: isHidden ? -100 : 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
className="fixed top-0 left-0 right-0 z-50 transition-all duration-300 bg-white/80 dark:bg-gray-800/80 backdrop-blur-lg border-b border-gray-200/50 dark:border-gray-700/50"
style={{ willChange: 'transform' }}
>
<div className={`
max-w-7xl mx-auto px-4 sm:px-6 lg:px-8
${isScrolled ? 'py-3' : 'py-4'}
transition-all duration-300
`}>
<div className="flex justify-between items-center">
{/* Logo */}
<motion.div
className="flex items-center"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Link href="/" className="flex items-center space-x-3 group">
<motion.div
className="w-10 h-10 bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600 rounded-xl flex items-center justify-center shadow-lg"
whileHover={{ rotate: 5, scale: 1.05 }}
transition={{ type: 'spring', stiffness: 300 }}
>
<span className="text-white font-bold text-lg">C</span>
</motion.div>
<motion.span
className="text-2xl font-black bg-gradient-to-r from-orange-400 to-orange-600 bg-clip-text text-transparent"
whileHover={{ scale: 1.02 }}
>
CarrotSkin
</motion.span>
</Link>
</motion.div>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-6">
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Link
href="/"
className="text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-all duration-200 font-medium relative group px-3 py-2 rounded-lg hover:bg-orange-500/10 dark:hover:bg-orange-400/10"
>
<motion.span
className="absolute -bottom-0.5 left-3 right-3 h-0.5 bg-gradient-to-r from-orange-400 to-orange-600"
initial={{ scaleX: 0 }}
whileHover={{ scaleX: 1 }}
transition={{ duration: 0.2 }}
/>
</Link>
</motion.div>
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Link
href="/skins"
className="text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-all duration-200 font-medium relative group px-3 py-2 rounded-lg hover:bg-orange-500/10 dark:hover:bg-orange-400/10"
>
<motion.span
className="absolute -bottom-0.5 left-3 right-3 h-0.5 bg-gradient-to-r from-orange-400 to-orange-600"
initial={{ scaleX: 0 }}
whileHover={{ scaleX: 1 }}
transition={{ duration: 0.2 }}
/>
</Link>
</motion.div>
{/* 用户头像框 - 类似知乎和哔哩哔哩的设计 */}
{isAuthenticated ? (
<div className="flex items-center space-x-4">
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Link
href="/profile"
className="flex items-center space-x-3 group"
onClick={handleLinkClick}
>
{user?.avatar ? (
<motion.div
className="relative"
whileHover={{ scale: 1.1 }}
>
<img
src={user.avatar}
alt={user.username}
className="w-9 h-9 rounded-full border-2 border-orange-500/30 group-hover:border-orange-500 transition-all duration-200 shadow-md"
/>
<motion.div
className="absolute inset-0 rounded-full bg-gradient-to-br from-orange-400/20 to-orange-600/20"
initial={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
transition={{ duration: 0.2 }}
/>
</motion.div>
) : (
<motion.div
className="relative"
whileHover={{ scale: 1.1 }}
>
<UserCircleIcon className="w-9 h-9 text-gray-400 group-hover:text-orange-500 transition-all duration-200" />
<motion.div
className="absolute inset-0 rounded-full bg-gradient-to-br from-orange-400/20 to-orange-600/20"
initial={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
transition={{ duration: 0.2 }}
/>
</motion.div>
)}
<motion.span
className="text-gray-700 dark:text-gray-300 group-hover:text-orange-500 dark:group-hover:text-orange-400 transition-all duration-200 font-medium"
whileHover={{ scale: 1.02 }}
>
{user?.username}
</motion.span>
</Link>
</motion.div>
<motion.button
onClick={handleLogout}
className="relative overflow-hidden border-2 border-orange-500 text-orange-500 hover:text-white font-medium py-2 px-4 rounded-lg transition-all duration-200 group"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<motion.span
className="absolute inset-0 w-0 bg-gradient-to-r from-orange-400 to-orange-600 transition-all duration-300 group-hover:w-full"
initial={{ width: 0 }}
whileHover={{ width: '100%' }}
/>
<span className="relative z-10">退</span>
</motion.button>
</div>
) : (
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Link
href="/auth"
className="flex items-center space-x-2 text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-all duration-200 group"
>
<motion.div
className="relative"
whileHover={{ scale: 1.1 }}
>
<UserCircleIcon className="w-7 h-7 text-gray-400 group-hover:text-orange-500 transition-all duration-200" />
<motion.div
className="absolute inset-0 rounded-full bg-gradient-to-br from-orange-400/20 to-orange-600/20"
initial={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
transition={{ duration: 0.2 }}
/>
</motion.div>
<span className="font-medium"></span>
</Link>
</motion.div>
)}
</div>
{/* Mobile menu button */}
<div className="md:hidden flex items-center">
<motion.button
onClick={() => setIsOpen(!isOpen)}
className="text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-colors duration-200 p-2"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{isOpen ? <XMarkIcon className="w-6 h-6" /> : <Bars3Icon className="w-6 h-6" />}
</motion.button>
</div>
</div>
</div>
{/* Mobile Navigation */}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, ease: 'easeInOut' }}
className="md:hidden bg-white/95 dark:bg-gray-900/95 backdrop-blur-md border-t border-gray-200/50 dark:border-gray-700/50"
>
<div className="px-4 py-4 space-y-1">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.05 }}
>
<Link
href="/"
className="block px-4 py-3 text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 rounded-lg transition-all duration-200 font-medium"
onClick={handleLinkClick}
>
</Link>
</motion.div>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
>
<Link
href="/skins"
className="block px-4 py-3 text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 rounded-lg transition-all duration-200 font-medium"
onClick={handleLinkClick}
>
</Link>
</motion.div>
{isAuthenticated ? (
<>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
>
<Link
href="/profile"
className="block px-4 py-3 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 rounded-lg transition-all duration-200"
onClick={handleLinkClick}
>
<div className="flex items-center space-x-3">
{user?.avatar ? (
<img
src={user.avatar}
alt={user.username}
className="w-8 h-8 rounded-full border-2 border-orange-500/30"
/>
) : (
<UserCircleIcon className="w-8 h-8 text-gray-400" />
)}
<span className="text-gray-700 dark:text-gray-300 font-medium">{user?.username}</span>
</div>
</Link>
</motion.div>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.25 }}
>
<button
onClick={handleLogout}
className="block w-full text-left px-4 py-3 text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 rounded-lg transition-all duration-200 font-medium"
>
退
</button>
</motion.div>
</>
) : (
<>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
>
<Link
href="/auth"
className="block px-4 py-3 text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 rounded-lg transition-all duration-200 font-medium"
onClick={handleLinkClick}
>
</Link>
</motion.div>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.25 }}
>
<Link
href="/auth"
className="block px-4 py-3 bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl font-medium text-center"
onClick={handleLinkClick}
>
</Link>
</motion.div>
</>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.nav>
);
}