Skip to content

Commit

Permalink
feat: 初步实现header,动态主题有问题
Browse files Browse the repository at this point in the history
  • Loading branch information
coderz-w committed Nov 1, 2024
1 parent a7381a0 commit 312cfde
Show file tree
Hide file tree
Showing 9 changed files with 438 additions and 3 deletions.
40 changes: 40 additions & 0 deletions src/components/layout/Header/AnimatedLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client';

import { useRouter, usePathname } from 'next/navigation';
import Image from 'next/image';
import { useMemo } from 'react';
import { m } from 'framer-motion';

export const AnimatedLogo = () => {
const router = useRouter();
const pathName = usePathname();
const handleClick = useMemo(() => () => router.push('/'), [pathName]);

return (
<button className=" cursor-pointer" onClick={() => handleClick()}>
<SideOwnerAvatar />
<span className="sr-only">Owner Avatar</span>
</button>
);
};

const SideOwnerAvatar = () => {
return (
<m.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="pointer-events-none relative z-[9] size-[40px] select-none"
>
<div className=" mask mask-squircle overflow-hidden">
<Image
src="/image/owner.jpg"
alt="Site Owner Avatar"
width={40}
height={40}
className="ring-2 ring-slate-200 dark:ring-neutral-800"
/>
</div>
</m.div>
);
};
14 changes: 14 additions & 0 deletions src/components/layout/Header/BluredBackground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import clsx from 'clsx';

export const BlurredBackground = () => {
return (
<div
className={clsx(
'absolute inset-0 transform-gpu [-webkit-backdrop-filter:saturate(180%)_blur(20px)] [backdrop-filter:saturate(180%)_blur(20px)] [backface-visibility:hidden]',
'bg-themed-bg_opacity [border-bottom:1px_solid_rgb(187_187_187_/_20%)]',
)}
/>
);
};
40 changes: 38 additions & 2 deletions src/components/layout/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
export const Header = () => {
return <div>哈哈哈哈111111111</div>;
import { memo } from 'react';

import { HeaderWithShadow } from './HeaderWithShadow';
import { BlurredBackground } from './BluredBackground';
import { HeaderCenterArea, HeaderLeftButtonArea, HeaderLogoArea } from './HeaderArea';
import { HeaderCenterContent } from './HeaderCenterContent';
import { AnimatedLogo } from './AnimatedLogo';
import styles from './header.module.css';

import { cn } from '@/lib/helper';

const Header = () => {
return (
<HeaderWithShadow>
<BlurredBackground />
<div
className={cn(
' relative mx-auto grid h-full min-h-0 max-w-7xl grid-cols-[4.5rem_auto_4.5rem] lg:px-8',
styles['header--grid'],
)}
>
<HeaderLeftButtonArea>小菜单</HeaderLeftButtonArea>

<HeaderLogoArea>
<AnimatedLogo />
</HeaderLogoArea>
<div className=" sr-only"></div>
<HeaderCenterArea>
<HeaderCenterContent />
{/* TODO 文章页面时显示一些文章信息 */}
</HeaderCenterArea>

{/* <div className="flex size-full [grid-area:right] items-center"></div> */}
</div>
</HeaderWithShadow>
);
};

export default memo(Header);
27 changes: 27 additions & 0 deletions src/components/layout/Header/HeaderArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { PropsWithChildren } from 'react';

import styles from './header.module.css';

import { cn } from '@/lib/helper';

export const HeaderLogoArea = ({ children }: PropsWithChildren) => (
<div className={cn('relative', styles['header--grid__logo'])}>
<div className={cn('relative flex size-full items-center justify-center')}>{children}</div>
</div>
);

export const HeaderLeftButtonArea = ({ children }: PropsWithChildren) => (
<div
className={cn('relative flex size-full items-center justify-center [grid-area:left] lg:hidden')}
>
{children}
</div>
);

export const HeaderCenterArea = ({ children }: PropsWithChildren) => (
<div className=" hidden [grid-area:center] lg:flex min-w-0 grow">
<div className="relative flex grow items-center justify-center">{children}</div>
</div>
);
241 changes: 241 additions & 0 deletions src/components/layout/Header/HeaderCenterContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
'use client';

import { LayoutGroup, m, useMotionTemplate, useMotionValue } from 'framer-motion';
import React, { memo } from 'react';
import { usePathname } from 'next/navigation';
import Link from 'next/link';

import { MenuPopover } from './MenuPopover';

import {
FaSolidCircleNotch,
FaSolidComments,
FaSolidDotCircle,
FaSolidFeatherAlt,
FaSolidHistory,
FaSolidUserFriends,
IcTwotoneSignpost,
IonBook,
MdiFlask,
} from '@/components/icons/menu-collection';
import { cn } from '@/lib/helper';

export const HeaderCenterContent = () => {
return (
<LayoutGroup>
<AnimatedMenu>
<DesktopNav />
</AnimatedMenu>
</LayoutGroup>
);
};

const AnimatedMenu = ({ children }: { children: React.ReactElement }) => {
//TODO 根据滚动值来控制动画
return (
<m.div
className="duration-100"
style={{
opacity: 1,
visibility: 'visible',
}}
>
{React.cloneElement(children, {})}
</m.div>
);
};

const DesktopNav = () => {
const pathname = usePathname();
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const radius = useMotionValue(0);
const handleMouseMove = React.useCallback(
({ clientX, clientY, currentTarget }: React.MouseEvent) => {
const bounds = currentTarget.getBoundingClientRect();
mouseX.set(clientX - bounds.left);
mouseY.set(clientY - bounds.top);
radius.set(Math.hypot(bounds.width, bounds.height) / 2.5);
},
[mouseX, mouseY, radius],
);

const background = useMotionTemplate`radial-gradient(${radius}px circle at ${mouseX}px ${mouseY}px, var(--spotlight-color) 0%, transparent 65%)`;

return (
<m.nav
layout="size"
onMouseMove={handleMouseMove}
className={cn(
'relative',
'rounded-full bg-gradient-to-b from-zinc-50/70 to-white/90',
'shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur-md',
'dark:from-zinc-900/70 dark:to-zinc-800/90 dark:ring-zinc-100/10',
'group [--spotlight-color:oklch(var(--a)_/_0.32)]',
'pointer-events-auto duration-200',
// shouldHideNavBg && '!bg-none !shadow-none !ring-transparent',
)}
>
{/* hover背景效果 */}
<m.div
className="pointer-events-none absolute -inset-px rounded-full opacity-0 transition-opacity duration-500 group-hover:opacity-100"
style={{ background }}
aria-hidden="true"
/>
<div className="flex px-4 font-medium text-zinc-800 dark:text-zinc-200">
{headerMenuConfig.map((section) => {
const subItemActive =
section.subMenu?.findIndex((item) => {
return item.path === pathname || pathname.slice(1) === item.path;
}) ?? -1;

return (
<HeaderMenuItem
section={section}
key={section.path}
subItemActive={section.subMenu?.[subItemActive]}
isActive={
pathname === section.path ||
pathname.startsWith(`${section.path}/`) ||
subItemActive > -1 ||
false
}
/>
);
})}
</div>
</m.nav>
);
};

const HeaderMenuItem = memo<{
section: IHeaderMenu;
isActive: boolean;
subItemActive?: IHeaderMenu;
}>(({ section, isActive, subItemActive }) => {
const href = section.path;

return (
<MenuPopover subMenu={section.subMenu} key={href}>
<AnimatedItem href={href} isActive={isActive} className="transition-[padding]">
<span className="relative flex items-center">
{isActive && (
<m.span layoutId="header-menu-icon" className="mr-2 flex items-center">
{subItemActive?.icon ?? section.icon}
</m.span>
)}
<m.span layout>{subItemActive?.title ?? section.title}</m.span>
</span>
</AnimatedItem>
</MenuPopover>
);
});

function AnimatedItem({
href,
children,
className,
isActive,
}: {
href: string;
children: React.ReactNode;
className?: string;
isActive?: boolean;
}) {
return (
<div>
<Link
href={href}
className={cn(
'relative block whitespace-nowrap px-4 py-2 transition',
isActive ? 'text-accent' : 'hover:text-accent/80',
isActive ? 'active' : '',
className,
)}
>
{children}
{isActive && (
<m.span
className={cn(
'absolute inset-x-1 -bottom-px h-px',
'bg-gradient-to-r from-accent/0 via-accent/70 to-accent/0',
)}
layoutId="active-nav-item"
/>
)}
</Link>
</div>
);
}

interface IHeaderMenu {
title: string;
path: string;
type?: string;
icon?: React.ReactNode;
subMenu?: IHeaderMenu[];
}

const headerMenuConfig: IHeaderMenu[] = [
{
title: '首页',
path: '/',
type: 'Home',
icon: React.createElement(FaSolidDotCircle),
subMenu: [],
},
{
title: '文稿',
path: '/posts',
type: 'Post',
subMenu: [],
icon: React.createElement(IcTwotoneSignpost),
},
{
title: '手记',
type: 'Note',
path: '/notes',
icon: React.createElement(FaSolidFeatherAlt),
},

{
title: '时光',
icon: React.createElement(FaSolidHistory),
path: '/timeline',
subMenu: [
{
title: '手记',
icon: React.createElement(FaSolidFeatherAlt),
path: '/timeline?type=note',
},
{
title: '文稿',
icon: React.createElement(IonBook),
path: '/timeline?type=post',
},
],
},
{
title: '友链',
icon: React.createElement(FaSolidUserFriends),
path: '/friends',
},

{
title: '更多',
icon: React.createElement(FaSolidCircleNotch),
path: '#',
subMenu: [
{
title: '项目',
icon: React.createElement(MdiFlask),
path: '/projects',
},
{
title: '自述',
path: '/about',
icon: React.createElement(FaSolidComments),
},
],
},
];
17 changes: 17 additions & 0 deletions src/components/layout/Header/HeaderWithShadow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import { PropsWithChildren } from 'react';
import clsx from 'clsx';

export const HeaderWithShadow = ({ children }: PropsWithChildren) => {
return (
<header
className={clsx(
'fixed inset-x-0 top-0 z-[9] mr-[var(--removed-body-scroll-bar-size)] h-[4.5rem] overflow-hidden transition-shadow duration-200',
'shadow-none shadow-neutral-100 dark:shadow-neutral-800/50 lg:shadow-sm',
)}
>
{children}
</header>
);
};
Loading

0 comments on commit 312cfde

Please sign in to comment.