Skip to content

Commit 312cfde

Browse files
committed
feat: 初步实现header,动态主题有问题
1 parent a7381a0 commit 312cfde

File tree

9 files changed

+438
-3
lines changed

9 files changed

+438
-3
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use client';
2+
3+
import { useRouter, usePathname } from 'next/navigation';
4+
import Image from 'next/image';
5+
import { useMemo } from 'react';
6+
import { m } from 'framer-motion';
7+
8+
export const AnimatedLogo = () => {
9+
const router = useRouter();
10+
const pathName = usePathname();
11+
const handleClick = useMemo(() => () => router.push('/'), [pathName]);
12+
13+
return (
14+
<button className=" cursor-pointer" onClick={() => handleClick()}>
15+
<SideOwnerAvatar />
16+
<span className="sr-only">Owner Avatar</span>
17+
</button>
18+
);
19+
};
20+
21+
const SideOwnerAvatar = () => {
22+
return (
23+
<m.div
24+
initial={{ opacity: 0, y: 10 }}
25+
animate={{ opacity: 1, y: 0 }}
26+
exit={{ opacity: 0, y: -10 }}
27+
className="pointer-events-none relative z-[9] size-[40px] select-none"
28+
>
29+
<div className=" mask mask-squircle overflow-hidden">
30+
<Image
31+
src="/image/owner.jpg"
32+
alt="Site Owner Avatar"
33+
width={40}
34+
height={40}
35+
className="ring-2 ring-slate-200 dark:ring-neutral-800"
36+
/>
37+
</div>
38+
</m.div>
39+
);
40+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use client';
2+
3+
import clsx from 'clsx';
4+
5+
export const BlurredBackground = () => {
6+
return (
7+
<div
8+
className={clsx(
9+
'absolute inset-0 transform-gpu [-webkit-backdrop-filter:saturate(180%)_blur(20px)] [backdrop-filter:saturate(180%)_blur(20px)] [backface-visibility:hidden]',
10+
'bg-themed-bg_opacity [border-bottom:1px_solid_rgb(187_187_187_/_20%)]',
11+
)}
12+
/>
13+
);
14+
};
Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1-
export const Header = () => {
2-
return <div>哈哈哈哈111111111</div>;
1+
import { memo } from 'react';
2+
3+
import { HeaderWithShadow } from './HeaderWithShadow';
4+
import { BlurredBackground } from './BluredBackground';
5+
import { HeaderCenterArea, HeaderLeftButtonArea, HeaderLogoArea } from './HeaderArea';
6+
import { HeaderCenterContent } from './HeaderCenterContent';
7+
import { AnimatedLogo } from './AnimatedLogo';
8+
import styles from './header.module.css';
9+
10+
import { cn } from '@/lib/helper';
11+
12+
const Header = () => {
13+
return (
14+
<HeaderWithShadow>
15+
<BlurredBackground />
16+
<div
17+
className={cn(
18+
' relative mx-auto grid h-full min-h-0 max-w-7xl grid-cols-[4.5rem_auto_4.5rem] lg:px-8',
19+
styles['header--grid'],
20+
)}
21+
>
22+
<HeaderLeftButtonArea>小菜单</HeaderLeftButtonArea>
23+
24+
<HeaderLogoArea>
25+
<AnimatedLogo />
26+
</HeaderLogoArea>
27+
<div className=" sr-only"></div>
28+
<HeaderCenterArea>
29+
<HeaderCenterContent />
30+
{/* TODO 文章页面时显示一些文章信息 */}
31+
</HeaderCenterArea>
32+
33+
{/* <div className="flex size-full [grid-area:right] items-center"></div> */}
34+
</div>
35+
</HeaderWithShadow>
36+
);
337
};
38+
39+
export default memo(Header);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client';
2+
3+
import { PropsWithChildren } from 'react';
4+
5+
import styles from './header.module.css';
6+
7+
import { cn } from '@/lib/helper';
8+
9+
export const HeaderLogoArea = ({ children }: PropsWithChildren) => (
10+
<div className={cn('relative', styles['header--grid__logo'])}>
11+
<div className={cn('relative flex size-full items-center justify-center')}>{children}</div>
12+
</div>
13+
);
14+
15+
export const HeaderLeftButtonArea = ({ children }: PropsWithChildren) => (
16+
<div
17+
className={cn('relative flex size-full items-center justify-center [grid-area:left] lg:hidden')}
18+
>
19+
{children}
20+
</div>
21+
);
22+
23+
export const HeaderCenterArea = ({ children }: PropsWithChildren) => (
24+
<div className=" hidden [grid-area:center] lg:flex min-w-0 grow">
25+
<div className="relative flex grow items-center justify-center">{children}</div>
26+
</div>
27+
);
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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+
];
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use client';
2+
3+
import { PropsWithChildren } from 'react';
4+
import clsx from 'clsx';
5+
6+
export const HeaderWithShadow = ({ children }: PropsWithChildren) => {
7+
return (
8+
<header
9+
className={clsx(
10+
'fixed inset-x-0 top-0 z-[9] mr-[var(--removed-body-scroll-bar-size)] h-[4.5rem] overflow-hidden transition-shadow duration-200',
11+
'shadow-none shadow-neutral-100 dark:shadow-neutral-800/50 lg:shadow-sm',
12+
)}
13+
>
14+
{children}
15+
</header>
16+
);
17+
};

0 commit comments

Comments
 (0)