|
| 1 | +'use client'; |
| 2 | + |
| 3 | +import { LayoutGroup, m, useMotionTemplate, useMotionValue } from 'framer-motion'; |
| 4 | +import React, { memo } from 'react'; |
| 5 | +import { usePathname } from 'next/navigation'; |
| 6 | +import Link from 'next/link'; |
| 7 | + |
| 8 | +import { MenuPopover } from './MenuPopover'; |
| 9 | + |
| 10 | +import { |
| 11 | + FaSolidCircleNotch, |
| 12 | + FaSolidComments, |
| 13 | + FaSolidDotCircle, |
| 14 | + FaSolidFeatherAlt, |
| 15 | + FaSolidHistory, |
| 16 | + FaSolidUserFriends, |
| 17 | + IcTwotoneSignpost, |
| 18 | + IonBook, |
| 19 | + MdiFlask, |
| 20 | +} from '@/components/icons/menu-collection'; |
| 21 | +import { cn } from '@/lib/helper'; |
| 22 | + |
| 23 | +export const HeaderCenterContent = () => { |
| 24 | + return ( |
| 25 | + <LayoutGroup> |
| 26 | + <AnimatedMenu> |
| 27 | + <DesktopNav /> |
| 28 | + </AnimatedMenu> |
| 29 | + </LayoutGroup> |
| 30 | + ); |
| 31 | +}; |
| 32 | + |
| 33 | +const AnimatedMenu = ({ children }: { children: React.ReactElement }) => { |
| 34 | + //TODO 根据滚动值来控制动画 |
| 35 | + return ( |
| 36 | + <m.div |
| 37 | + className="duration-100" |
| 38 | + style={{ |
| 39 | + opacity: 1, |
| 40 | + visibility: 'visible', |
| 41 | + }} |
| 42 | + > |
| 43 | + {React.cloneElement(children, {})} |
| 44 | + </m.div> |
| 45 | + ); |
| 46 | +}; |
| 47 | + |
| 48 | +const DesktopNav = () => { |
| 49 | + const pathname = usePathname(); |
| 50 | + const mouseX = useMotionValue(0); |
| 51 | + const mouseY = useMotionValue(0); |
| 52 | + const radius = useMotionValue(0); |
| 53 | + const handleMouseMove = React.useCallback( |
| 54 | + ({ clientX, clientY, currentTarget }: React.MouseEvent) => { |
| 55 | + const bounds = currentTarget.getBoundingClientRect(); |
| 56 | + mouseX.set(clientX - bounds.left); |
| 57 | + mouseY.set(clientY - bounds.top); |
| 58 | + radius.set(Math.hypot(bounds.width, bounds.height) / 2.5); |
| 59 | + }, |
| 60 | + [mouseX, mouseY, radius], |
| 61 | + ); |
| 62 | + |
| 63 | + const background = useMotionTemplate`radial-gradient(${radius}px circle at ${mouseX}px ${mouseY}px, var(--spotlight-color) 0%, transparent 65%)`; |
| 64 | + |
| 65 | + return ( |
| 66 | + <m.nav |
| 67 | + layout="size" |
| 68 | + onMouseMove={handleMouseMove} |
| 69 | + className={cn( |
| 70 | + 'relative', |
| 71 | + 'rounded-full bg-gradient-to-b from-zinc-50/70 to-white/90', |
| 72 | + 'shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur-md', |
| 73 | + 'dark:from-zinc-900/70 dark:to-zinc-800/90 dark:ring-zinc-100/10', |
| 74 | + 'group [--spotlight-color:oklch(var(--a)_/_0.32)]', |
| 75 | + 'pointer-events-auto duration-200', |
| 76 | + // shouldHideNavBg && '!bg-none !shadow-none !ring-transparent', |
| 77 | + )} |
| 78 | + > |
| 79 | + {/* hover背景效果 */} |
| 80 | + <m.div |
| 81 | + className="pointer-events-none absolute -inset-px rounded-full opacity-0 transition-opacity duration-500 group-hover:opacity-100" |
| 82 | + style={{ background }} |
| 83 | + aria-hidden="true" |
| 84 | + /> |
| 85 | + <div className="flex px-4 font-medium text-zinc-800 dark:text-zinc-200"> |
| 86 | + {headerMenuConfig.map((section) => { |
| 87 | + const subItemActive = |
| 88 | + section.subMenu?.findIndex((item) => { |
| 89 | + return item.path === pathname || pathname.slice(1) === item.path; |
| 90 | + }) ?? -1; |
| 91 | + |
| 92 | + return ( |
| 93 | + <HeaderMenuItem |
| 94 | + section={section} |
| 95 | + key={section.path} |
| 96 | + subItemActive={section.subMenu?.[subItemActive]} |
| 97 | + isActive={ |
| 98 | + pathname === section.path || |
| 99 | + pathname.startsWith(`${section.path}/`) || |
| 100 | + subItemActive > -1 || |
| 101 | + false |
| 102 | + } |
| 103 | + /> |
| 104 | + ); |
| 105 | + })} |
| 106 | + </div> |
| 107 | + </m.nav> |
| 108 | + ); |
| 109 | +}; |
| 110 | + |
| 111 | +const HeaderMenuItem = memo<{ |
| 112 | + section: IHeaderMenu; |
| 113 | + isActive: boolean; |
| 114 | + subItemActive?: IHeaderMenu; |
| 115 | +}>(({ section, isActive, subItemActive }) => { |
| 116 | + const href = section.path; |
| 117 | + |
| 118 | + return ( |
| 119 | + <MenuPopover subMenu={section.subMenu} key={href}> |
| 120 | + <AnimatedItem href={href} isActive={isActive} className="transition-[padding]"> |
| 121 | + <span className="relative flex items-center"> |
| 122 | + {isActive && ( |
| 123 | + <m.span layoutId="header-menu-icon" className="mr-2 flex items-center"> |
| 124 | + {subItemActive?.icon ?? section.icon} |
| 125 | + </m.span> |
| 126 | + )} |
| 127 | + <m.span layout>{subItemActive?.title ?? section.title}</m.span> |
| 128 | + </span> |
| 129 | + </AnimatedItem> |
| 130 | + </MenuPopover> |
| 131 | + ); |
| 132 | +}); |
| 133 | + |
| 134 | +function AnimatedItem({ |
| 135 | + href, |
| 136 | + children, |
| 137 | + className, |
| 138 | + isActive, |
| 139 | +}: { |
| 140 | + href: string; |
| 141 | + children: React.ReactNode; |
| 142 | + className?: string; |
| 143 | + isActive?: boolean; |
| 144 | +}) { |
| 145 | + return ( |
| 146 | + <div> |
| 147 | + <Link |
| 148 | + href={href} |
| 149 | + className={cn( |
| 150 | + 'relative block whitespace-nowrap px-4 py-2 transition', |
| 151 | + isActive ? 'text-accent' : 'hover:text-accent/80', |
| 152 | + isActive ? 'active' : '', |
| 153 | + className, |
| 154 | + )} |
| 155 | + > |
| 156 | + {children} |
| 157 | + {isActive && ( |
| 158 | + <m.span |
| 159 | + className={cn( |
| 160 | + 'absolute inset-x-1 -bottom-px h-px', |
| 161 | + 'bg-gradient-to-r from-accent/0 via-accent/70 to-accent/0', |
| 162 | + )} |
| 163 | + layoutId="active-nav-item" |
| 164 | + /> |
| 165 | + )} |
| 166 | + </Link> |
| 167 | + </div> |
| 168 | + ); |
| 169 | +} |
| 170 | + |
| 171 | +interface IHeaderMenu { |
| 172 | + title: string; |
| 173 | + path: string; |
| 174 | + type?: string; |
| 175 | + icon?: React.ReactNode; |
| 176 | + subMenu?: IHeaderMenu[]; |
| 177 | +} |
| 178 | + |
| 179 | +const headerMenuConfig: IHeaderMenu[] = [ |
| 180 | + { |
| 181 | + title: '首页', |
| 182 | + path: '/', |
| 183 | + type: 'Home', |
| 184 | + icon: React.createElement(FaSolidDotCircle), |
| 185 | + subMenu: [], |
| 186 | + }, |
| 187 | + { |
| 188 | + title: '文稿', |
| 189 | + path: '/posts', |
| 190 | + type: 'Post', |
| 191 | + subMenu: [], |
| 192 | + icon: React.createElement(IcTwotoneSignpost), |
| 193 | + }, |
| 194 | + { |
| 195 | + title: '手记', |
| 196 | + type: 'Note', |
| 197 | + path: '/notes', |
| 198 | + icon: React.createElement(FaSolidFeatherAlt), |
| 199 | + }, |
| 200 | + |
| 201 | + { |
| 202 | + title: '时光', |
| 203 | + icon: React.createElement(FaSolidHistory), |
| 204 | + path: '/timeline', |
| 205 | + subMenu: [ |
| 206 | + { |
| 207 | + title: '手记', |
| 208 | + icon: React.createElement(FaSolidFeatherAlt), |
| 209 | + path: '/timeline?type=note', |
| 210 | + }, |
| 211 | + { |
| 212 | + title: '文稿', |
| 213 | + icon: React.createElement(IonBook), |
| 214 | + path: '/timeline?type=post', |
| 215 | + }, |
| 216 | + ], |
| 217 | + }, |
| 218 | + { |
| 219 | + title: '友链', |
| 220 | + icon: React.createElement(FaSolidUserFriends), |
| 221 | + path: '/friends', |
| 222 | + }, |
| 223 | + |
| 224 | + { |
| 225 | + title: '更多', |
| 226 | + icon: React.createElement(FaSolidCircleNotch), |
| 227 | + path: '#', |
| 228 | + subMenu: [ |
| 229 | + { |
| 230 | + title: '项目', |
| 231 | + icon: React.createElement(MdiFlask), |
| 232 | + path: '/projects', |
| 233 | + }, |
| 234 | + { |
| 235 | + title: '自述', |
| 236 | + path: '/about', |
| 237 | + icon: React.createElement(FaSolidComments), |
| 238 | + }, |
| 239 | + ], |
| 240 | + }, |
| 241 | +]; |
0 commit comments