Skip to content

Commit

Permalink
Merge pull request #106 from JohnsonMao/refactor/header-component
Browse files Browse the repository at this point in the history
♻️ refactor header component computed scroll
💄 update scroll padding top
♻️ split Menu into separate component in Header
✅ update unit test
  • Loading branch information
JohnsonMao authored Oct 20, 2023
2 parents 5d9ec32 + 3fe9c24 commit 73cff25
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 192 deletions.
4 changes: 2 additions & 2 deletions src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async function I18nLayout({
params: { lang },
}: React.PropsWithChildren & RootParams) {
const { common } = await getDictionary(lang);
const logo = {
const avatar = {
src: avatarUrl,
alt: name,
};
Expand All @@ -55,7 +55,7 @@ async function I18nLayout({

return (
<>
<Header logo={logo} menu={menu} />
<Header avatar={avatar} menu={menu} />
{children}
<Footer copyright={copyright} />
</>
Expand Down
10 changes: 6 additions & 4 deletions src/app/[lang]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ async function RootPage({ params: { lang } }: RootParams) {

return (
<Container as="main">
<H1 className="pb-6 pl-16 pt-12 text-3xl font-bold dark:text-white">
{metadata.title}
</H1>
<p className="pb-16 text-xl dark:text-white">{metadata.description}</p>
<div className="py-4">
<H1 className="pb-4 pt-16 text-3xl font-bold dark:text-white">
{metadata.title}
</H1>
<p className="pb-16 text-xl dark:text-white">{metadata.description}</p>
</div>
<List Item={Card} items={posts} />
</Container>
);
Expand Down
8 changes: 5 additions & 3 deletions src/app/[lang]/posts/[postId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ async function PostPage({ params: { postId } }: PostParams) {
return (
<>
<Container as="main">
<H1 className="pb-6 pl-16 pt-12 text-3xl font-bold dark:text-white">
{frontmatter.title}
</H1>
<div className="py-4">
<H1 className="pb-4 pt-16 text-3xl font-bold dark:text-white">
{frontmatter.title}
</H1>
</div>
<time className="mt-0">{formattedDate}</time>
<article className="prose prose-xl prose-slate mx-auto dark:prose-invert">
{content}
Expand Down
10 changes: 6 additions & 4 deletions src/app/[lang]/posts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ async function RootPage({ params: { lang } }: RootParams) {

return (
<Container as="main">
<H1 className="pb-6 pl-16 pt-12 text-3xl font-bold dark:text-white">
{metadata.title}
</H1>
<p className="pb-16 text-xl dark:text-white">{metadata.description}</p>
<div className="py-4">
<H1 className="pb-4 pt-16 text-3xl font-bold dark:text-white">
{metadata.title}
</H1>
<p className="pb-16 text-xl dark:text-white">{metadata.description}</p>
</div>
<List Item={Card} items={posts} />
</Container>
);
Expand Down
2 changes: 1 addition & 1 deletion src/assets/css/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
@import "./prism-vsc-dark-plus.css";

:root {
scroll-padding-top: 72px;
scroll-padding-top: 2rem;
}
130 changes: 61 additions & 69 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,95 +1,87 @@
'use client';

import { usePathname } from 'next/navigation';
import { CSSProperties, useEffect, useState, useRef } from 'react';
import type { StaticImport } from 'next/dist/shared/lib/get-img-props';
import { CSSProperties, useRef } from 'react';

import useScroll from '@/hooks/useScroll';
import cn from '@/utils/cn';
import getLocale from '@/utils/getLocale';
import { clamp } from '@/utils/math';

import ThemeSwitcher from '../ThemeSwitcher';
import Link from '../Link';
import Logo, { LogoProps } from './Logo';
import Container from '../Container';
import Image from '../Image';
import Link from '../Link';
import ThemeSwitcher from '../ThemeSwitcher';
import Menu, { MenuProps } from './Menu';

type MenuItem = {
text: string;
href: LinkWithoutLocalePathProps['href'];
type Avatar = {
src: string | StaticImport;
alt: string;
};

export type HeaderProps = {
logo: Omit<LogoProps, 'scrollY' | 'scrollThreshold'>;
menu: MenuItem[];
};
avatar: Avatar;
scrollThreshold?: number;
} & MenuProps;

function Header({ logo, menu }: HeaderProps) {
const pathname = usePathname();
const [translateY, setTranslateY] = useState(0);
function Header({ avatar, menu, scrollThreshold = 100 }: HeaderProps) {
const [scroll] = useScroll();
const headerRef = useRef<HTMLDivElement>(null);
const headerHeight = useRef(0);
const headerTranslateY = useRef(0);
const previousScrollY = useRef(0);
const currentScrollY = Math.floor(scroll.y);
const scrollThreshold = 120;

useEffect(() => {
headerHeight.current = headerRef.current?.clientHeight || 0;
}, []);

useEffect(() => {
const scrollY = currentScrollY - scrollThreshold;

if (scrollY <= 0) return setTranslateY(0);

const deltaScrollY = previousScrollY.current - scrollY;

previousScrollY.current = scrollY;

setTranslateY((pre) =>
clamp(pre + deltaScrollY, headerHeight.current * -1)
);
}, [currentScrollY]);

const isActiveLink = (href: LinkWithoutLocalePathProps['href']) => {
const locale = getLocale(pathname) || '';
const localePrefix = new RegExp(`^/${locale}/?`);
const adjustedPathname = pathname.replace(localePrefix, '/');

if (adjustedPathname === '/') return href === '/';

return adjustedPathname.startsWith(href) && href !== '/';
const avatarTranslateY = (
clamp(currentScrollY * -1, scrollThreshold * -1) + scrollThreshold
).toFixed(2);
const avatarScale = (
1.5 -
clamp(currentScrollY, scrollThreshold) / (scrollThreshold * 2)
).toFixed(2);

const calcHeaderTranslateY = () => {
const scrollPosition = currentScrollY - scrollThreshold;

if (scrollPosition <= 0) {
headerTranslateY.current = 0;
} else {
const deltaScrollY = previousScrollY.current - scrollPosition;
const headerHeight = headerRef.current?.clientHeight || 0;

previousScrollY.current = scrollPosition;
headerTranslateY.current = clamp(
headerTranslateY.current + deltaScrollY,
headerHeight * -1
);
}

return headerTranslateY.current;
};

const headerStyles = {
'--header-translate-y': `${calcHeaderTranslateY()}px`,
'--avatar-translate-y': `${avatarTranslateY}px`,
'--avatar-scale': avatarScale,
} as CSSProperties;

return (
<Container
as="header"
ref={headerRef}
className="sticky top-0 z-10 flex translate-y-0 items-center justify-between py-7"
style={{ '--tw-translate-y': `${translateY}px` } as CSSProperties}
className="sticky top-0 z-10 flex translate-y-[var(--header-translate-y)] items-center justify-between py-7 will-change-transform"
style={headerStyles}
>
<Logo
{...logo}
scrollY={currentScrollY}
scrollThreshold={scrollThreshold}
/>
<nav>
<ul className="flex rounded-full bg-gray-900/60 px-2 backdrop-blur-md">
{menu.map(({ text, href }) => (
<li key={text}>
<Link
href={href}
className={cn(
'block p-3 text-xl leading-none text-white/90 no-underline hover:text-white',
isActiveLink(href) && 'text-blue-500 hover:text-blue-500'
)}
>
{text}
</Link>
</li>
))}
</ul>
</nav>
<Link
href="/"
className="origin-bottom-left translate-y-[var(--avatar-translate-y)] scale-[var(--avatar-scale)]"
>
<Image
className="rounded-full shadow-black drop-shadow-xl"
width={40}
height={40}
src={avatar.src}
alt={avatar.alt}
priority
/>
</Link>
<Menu menu={menu} />
<ThemeSwitcher className="rounded-full bg-gray-900/60 p-3 backdrop-blur-md" />
</Container>
);
Expand Down
48 changes: 0 additions & 48 deletions src/components/Header/Logo.tsx

This file was deleted.

53 changes: 53 additions & 0 deletions src/components/Header/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use client';

import { usePathname } from 'next/navigation';

import cn from '@/utils/cn';
import getLocale from '@/utils/getLocale';

import Link from '../Link';

type MenuItem = {
text: string;
href: LinkWithoutLocalePathProps['href'];
};

export type MenuProps = {
menu: MenuItem[];
};

function Menu({ menu }: MenuProps) {
const pathname = usePathname();

const isActiveLink = (href: LinkWithoutLocalePathProps['href']) => {
const locale = getLocale(pathname) || '';
const localePrefix = new RegExp(`^/${locale}/?`);
const adjustedPathname = pathname.replace(localePrefix, '/');

if (adjustedPathname === '/') return href === '/';

return adjustedPathname.startsWith(href) && href !== '/';
};

return (
<nav>
<ul className="flex rounded-full bg-gray-900/60 px-2 backdrop-blur-md">
{menu.map(({ text, href }) => (
<li key={text}>
<Link
href={href}
className={cn(
'block p-3 text-xl leading-none text-white/90 no-underline hover:text-white',
isActiveLink(href) && 'text-blue-500 hover:text-blue-500'
)}
>
{text}
</Link>
</li>
))}
</ul>
</nav>
);
}

export default Menu;
Loading

0 comments on commit 73cff25

Please sign in to comment.