Skip to content

Commit

Permalink
feat: mobile menu
Browse files Browse the repository at this point in the history
  • Loading branch information
martapanc-resourcify committed Jul 5, 2023
1 parent cc5deaf commit ef5b359
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 39 deletions.
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.13.5",
"classnames": "^2.3.2",
"clsx": "^1.2.1",
"focus-trap-react": "^10.1.4",
"framer-motion": "^10.12.18",
"inquirer-fuzzy-path": "^2.3.0",
"next": "^13.4.4",
"plop": "^3.1.2",
"next-themes": "^0.2.1",
"plop": "^3.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-headroom": "^3.2.1",
"react-icons": "^4.8.0",
"tailwind-merge": "^1.12.0"
},
Expand All @@ -49,6 +53,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@types/react": "^18.2.7",
"@types/react-headroom": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"autoprefixer": "^10.4.14",
Expand Down
22 changes: 22 additions & 0 deletions src/components/atoms/BurgerIcon/BurgerIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import classNames from 'classnames';

export interface BurgerIconProps {
isOpen: boolean;
}

const BurgerIcon = ({ isOpen }: BurgerIconProps) => {
return (
<div
className={classNames('burger-icon', {
open: isOpen,
})}
>
<span />
<span />
<span />
<span />
</div>
);
};

export { BurgerIcon };
1 change: 1 addition & 0 deletions src/components/atoms/BurgerIcon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './BurgerIcon';
50 changes: 50 additions & 0 deletions src/components/atoms/NavigationItem/NavigationItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';

import classNames from 'classnames';
import { motion, Variants } from 'framer-motion';
import Link from 'next/link';
import { usePathname } from 'next/navigation';

export interface NavigationItemProps {
href: string;
title: string;
variants: Variants;
initial: string;
animate: string;
customDelay?: number;
}

const NavigationItem = ({
href,
title,
variants,
initial,
animate,
customDelay,
}: NavigationItemProps) => {
const pathname = usePathname();
const isActive = pathname?.startsWith(href);

return (
<motion.li
variants={variants}
initial={initial}
animate={animate}
custom={customDelay}
>
<Link
href={href}
className={classNames(
isActive
? 'text-off-black dark:text-off-white font-bold'
: 'hover:text-off-black dark:hover:text-off-white font-medium text-slate-700 dark:text-slate-400 md:text-slate-500 md:dark:text-slate-400',
'md:underlined relative block whitespace-nowrap text-2xl transition md:text-lg'
)}
>
{title}
</Link>
</motion.li>
);
};

export { NavigationItem };
3 changes: 3 additions & 0 deletions src/components/atoms/NavigationItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use client';

export * from './NavigationItem';
116 changes: 79 additions & 37 deletions src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
'use client';

import { usePathname } from 'next/navigation';
import { useTheme } from 'next-themes';
import * as React from 'react';
import { useEffect, useState } from 'react';
import Headroom from 'react-headroom';

import { useOnKeyDown } from '@/hooks/useOnKeyDown';

import { BurgerIcon } from '@/components/atoms/BurgerIcon';
import ModeToggleButton from '@/components/buttons/ModeToggleButton';
import UnstyledLink from '@/components/links/UnstyledLink';
import { MobileMenu } from '@/components/molecules/MobileMenu/MobileMenu';

const links = [
export const links = [
{ href: '/about', label: 'About' },
{ href: '/projects', label: 'Projects' },
{ href: '/cv', label: 'CV' },
Expand All @@ -14,50 +23,83 @@ const links = [

export default function Header() {
const { theme } = useTheme();
const [isOpen, setIsOpen] = useState(false);

const pathname = usePathname();

useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
}

return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);

useEffect(() => {
setIsOpen(false);
}, [pathname]);

useOnKeyDown('Escape', () => setIsOpen(false));

return (
<header
className={`sticky top-0 z-50 py-6 ${
theme === 'dark'
? 'bg-gradient-to-r from-sky-900 to-blue-950'
: 'bg-gradient-to-r from-sky-300 to-blue-400'
}`}
>
<div className='layout flex h-12 w-full items-center justify-between'>
<nav className='m-4 flex w-full items-center justify-between'>
<UnstyledLink
href='/'
className={`font-bold
<Headroom style={{ zIndex: 50 }}>
<header
className={`sticky top-0 z-50 py-6 ${
theme === 'dark'
? 'bg-gradient-to-r from-sky-900 to-blue-950'
: 'bg-gradient-to-r from-sky-300 to-blue-400'
}`}
>
<div className='layout flex h-12 w-full items-center justify-between'>
<nav className='m-4 flex w-full items-center justify-between'>
<UnstyledLink
href='/'
className={`font-bold
${
theme === 'dark'
? 'text-blue-50 hover:text-blue-200'
: 'text-slate-900 hover:text-slate-700'
}
`}
>
Home
</UnstyledLink>

<ul className='hidden items-center justify-between space-x-6 text-lg md:flex'>
{links.map(({ href, label }) => (
<li key={`${href}${label}`}>
<UnstyledLink
href={href}
className={`${
theme === 'dark'
? 'text-blue-50 hover:text-blue-200'
: 'text-slate-900 hover:text-slate-700'
}`}
>
{label}
</UnstyledLink>
</li>
))}
</ul>

<div className='hidden md:block'>
<ModeToggleButton />
</div>
</nav>

<button
className='absolute right-4 top-8 z-50 md:hidden'
onClick={() => setIsOpen((prev) => !prev)}
aria-label='Menu'
>
Home
</UnstyledLink>

<ul className='flex items-center justify-between space-x-6 text-lg'>
{links.map(({ href, label }) => (
<li key={`${href}${label}`}>
<UnstyledLink
href={href}
className={`${
theme === 'dark'
? 'text-blue-50 hover:text-blue-200'
: 'text-slate-900 hover:text-slate-700'
}`}
>
{label}
</UnstyledLink>
</li>
))}
</ul>

<ModeToggleButton />
</nav>
</div>
</header>
<BurgerIcon isOpen={isOpen} />
</button>
</div>
</header>

<MobileMenu isOpen={isOpen} />
</Headroom>
);
}
67 changes: 67 additions & 0 deletions src/components/molecules/MobileMenu/MobileMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client';

import FocusTrap from 'focus-trap-react';
import { AnimatePresence, motion } from 'framer-motion';
import * as React from 'react';

import { NavigationItem } from '@/components/atoms/NavigationItem';
import ModeToggleButton from '@/components/buttons/ModeToggleButton';
import { links } from '@/components/layout/Header';

export interface MobileMenuProps {
isOpen: boolean;
}

export const MobileMenu = ({ isOpen }: MobileMenuProps) => {
const navigationVariants = {
hidden: { opacity: 0, x: -50 },
visible: (custom: number) => ({
opacity: 1,
x: 0,
transition: { delay: custom },
}),
};

return (
<AnimatePresence>
{isOpen ? (
<motion.div
className='from-grey-200 dark:from-grey-900 fixed top-0 z-40 h-screen w-screen gap-12 bg-gradient-to-b to-transparent p-4 backdrop-blur-xl transition-all delay-100 duration-700 ease-in-out md:hidden'
initial={{ opacity: 0, y: '-50%', x: 0 }}
animate={{ opacity: 1, y: 0, x: 0 }}
exit={{ opacity: 0, y: '-50%' }}
transition={{ duration: 0, delay: 0 }}
>
<FocusTrap
focusTrapOptions={{
clickOutsideDeactivates: true,
}}
>
<ul className='align-center flex h-full flex-col justify-center gap-4 text-center'>
{links.map(({ href, label }, i) => (
<NavigationItem
key={href}
href={href}
title={label}
variants={navigationVariants}
initial='hidden'
animate='visible'
customDelay={0.5 + (i + 1) * 0.1}
/>
))}
<motion.li
className='mt-12 flex justify-center'
variants={navigationVariants}
initial='hidden'
animate='visible'
custom={0.5 + (links.length + 1) * 0.1}
>
<ModeToggleButton />
</motion.li>
</ul>
</FocusTrap>
</motion.div>
) : null}
</AnimatePresence>
);
};
17 changes: 17 additions & 0 deletions src/hooks/useOnKeyDown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect } from 'react';

export const useOnKeyDown = (
key: KeyboardEvent['key'],
handler: () => void
) => {
useEffect(() => {
const callHandlerOnKeyDown = (e: KeyboardEvent) => {
if (e.key === key) {
handler();
}
};
window.addEventListener('keydown', callHandlerOnKeyDown);

return () => window.removeEventListener('keydown', callHandlerOnKeyDown);
}, [handler, key]);
};
Loading

0 comments on commit ef5b359

Please sign in to comment.