diff --git a/package.json b/package.json index 22fc8c22..555178ed 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "lint": "next lint", + "lint": "eslint src", "lint:fix": "eslint src --fix && pnpm format", "lint:strict": "eslint --max-warnings=0 src", "typecheck": "tsc --noEmit", diff --git a/src/__tests__/components/layout/Header.test.tsx b/src/__tests__/components/layout/Header.test.tsx index c860c746..1261a20a 100644 --- a/src/__tests__/components/layout/Header.test.tsx +++ b/src/__tests__/components/layout/Header.test.tsx @@ -59,11 +59,18 @@ describe('Header Component', () => { expect(articlesLink).toHaveAttribute('href', '/articles'); }); + it('should render Exhibition link', () => { + render(
); + const exhibitionLink = screen.getByRole('link', { name: /exhibition/i }); + expect(exhibitionLink).toBeInTheDocument(); + expect(exhibitionLink).toHaveAttribute('href', '/exhibition'); + }); + it('should have all navigation links', () => { render(
); const navLinks = screen.getAllByRole('link'); - // Should have at least 4 links (logo + 3 nav items) - expect(navLinks.length).toBeGreaterThanOrEqual(4); + // Should have at least 5 links (logo + 4 nav items) + expect(navLinks.length).toBeGreaterThanOrEqual(5); }); }); @@ -111,9 +118,11 @@ describe('Header Component', () => { const aboutLink = screen.getByRole('link', { name: /about/i }); const teamLink = screen.getByRole('link', { name: /team/i }); const articlesLink = screen.getByRole('link', { name: /articles/i }); + const exhibitionLink = screen.getByRole('link', { name: /exhibition/i }); expect(aboutLink).toBeInTheDocument(); expect(teamLink).toBeInTheDocument(); expect(articlesLink).toBeInTheDocument(); + expect(exhibitionLink).toBeInTheDocument(); }); }); @@ -127,7 +136,7 @@ describe('Header Component', () => { it('should render list items for each nav link', () => { render(
); const listItems = screen.getAllByRole('listitem'); - expect(listItems.length).toBe(3); // About, Team, Articles + expect(listItems.length).toBe(4); // About, Team, Articles, Exhibition }); it('should use NavLink components', () => { diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index e2c639bd..d2cbc0a9 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,4 +1,8 @@ +'use client'; + +import { Menu, X } from 'lucide-react'; import Link from 'next/link'; +import * as React from 'react'; import { Logo } from '@/components/ui/icons/Logo'; import NavLink from '@/components/ui/NavLink'; @@ -8,6 +12,7 @@ const navLinks = [ { href: '/about', text: 'About' }, { href: '/team', text: 'Team' }, { href: '/articles', text: 'Articles' }, + { href: '/exhibition', text: 'Exhibition' }, ]; function Navigation() { @@ -25,9 +30,38 @@ function Navigation() { } export default function Header() { + const [mobileNavOpen, setMobileNavOpen] = React.useState(false); + const mobileNavId = React.useId(); + const mobileNavRef = React.useRef(null); + + React.useEffect(() => { + if (!mobileNavOpen) return; + + function onKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') setMobileNavOpen(false); + } + + function onPointerDown(e: MouseEvent | TouchEvent) { + const target = e.target; + if (!(target instanceof Node)) return; + if (mobileNavRef.current?.contains(target)) return; + setMobileNavOpen(false); + } + + document.addEventListener('keydown', onKeyDown); + document.addEventListener('mousedown', onPointerDown); + document.addEventListener('touchstart', onPointerDown, { passive: true }); + + return () => { + document.removeEventListener('keydown', onKeyDown); + document.removeEventListener('mousedown', onPointerDown); + document.removeEventListener('touchstart', onPointerDown); + }; + }, [mobileNavOpen]); + return (
-
+
+
- +
+ +
+ + +
+ +
); diff --git a/src/components/ui/NavLink.tsx b/src/components/ui/NavLink.tsx index 35e3b740..2c67834b 100644 --- a/src/components/ui/NavLink.tsx +++ b/src/components/ui/NavLink.tsx @@ -9,9 +9,11 @@ import clsxm from '@/lib/clsxm'; const NavLink = ({ href, children, + onClick, }: { href: string; children: React.ReactNode; + onClick?: React.MouseEventHandler; }) => { const pathname = usePathname(); const isActive = href === '/' ? pathname === '/' : pathname.startsWith(href); @@ -19,6 +21,7 @@ const NavLink = ({ return (