Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState, useRef } from 'react'; | |
| import { Link, NavLink } from 'react-router-dom'; | |
| import clsx from 'clsx'; | |
| import ScrollProgress from './ScrollProgress.jsx'; | |
| import ImageFlex from './ImageFlex.jsx'; | |
| export default function Header() { | |
| const [scrolled, setScrolled] = useState(false); | |
| const [menuOpen, setMenuOpen] = useState(false); | |
| const [projectsOpen, setProjectsOpen] = useState(false); | |
| const [headerVisible, setHeaderVisible] = useState(false); | |
| const [isHovered, setIsHovered] = useState(false); | |
| const closeTimerRef = useRef(null); | |
| const headerRef = useRef(null); | |
| useEffect(() => { | |
| const onScroll = () => { | |
| const scrollY = window.scrollY; | |
| setScrolled(scrollY > 24); | |
| // Show header when scrolled | |
| setHeaderVisible(scrollY > 50); | |
| }; | |
| onScroll(); | |
| window.addEventListener('scroll', onScroll, { passive: true }); | |
| return () => window.removeEventListener('scroll', onScroll); | |
| }, []); | |
| useEffect(() => { | |
| // Show header on hover or when menu is open, regardless of scroll position | |
| if (isHovered || menuOpen) { | |
| setHeaderVisible(true); | |
| } else { | |
| // Only hide if not scrolled | |
| const scrollY = window.scrollY; | |
| if (scrollY <= 50) { | |
| setHeaderVisible(false); | |
| } | |
| } | |
| }, [isHovered, menuOpen]); | |
| useEffect(() => { | |
| document.body.style.overflow = menuOpen ? 'hidden' : ''; | |
| }, [menuOpen]); | |
| const navLink = (to, label) => ( | |
| <NavLink | |
| to={to} | |
| className={({ isActive }) => | |
| clsx( | |
| 'relative px-3 py-2 text-sm font-medium transition', | |
| // Animated underline (temporary) from right -> left on hover | |
| 'after:content-[""] after:absolute after:left-3 after:right-3 after:-bottom-1 after:h-[2px] after:bg-brand-600', | |
| 'after:transform after:scale-x-0 after:origin-right after:transition-transform after:duration-300', | |
| 'hover:after:origin-left hover:after:scale-x-100', | |
| isActive ? 'text-brand-700' : 'text-slate-700 hover:text-brand-700' | |
| ) | |
| } | |
| > | |
| {label} | |
| </NavLink> | |
| ); | |
| return ( | |
| <> | |
| {/* Invisible hover area at top of page to reveal header */} | |
| <div | |
| className="fixed inset-x-0 top-0 z-40 h-4" | |
| onMouseEnter={() => setIsHovered(true)} | |
| onMouseLeave={() => setIsHovered(false)} | |
| aria-hidden="true" | |
| /> | |
| <header | |
| ref={headerRef} | |
| onMouseEnter={() => setIsHovered(true)} | |
| onMouseLeave={() => setIsHovered(false)} | |
| className={clsx( | |
| 'fixed inset-x-0 top-0 z-50 bg-white transition-all duration-300', | |
| scrolled ? 'shadow' : '', | |
| headerVisible | |
| ? 'translate-y-0 opacity-100 pointer-events-auto' | |
| : '-translate-y-full opacity-0 pointer-events-none' | |
| )} | |
| role="banner" | |
| style={{ | |
| transform: headerVisible ? 'translateY(0)' : 'translateY(-100%)', | |
| }} | |
| > | |
| <ScrollProgress /> | |
| <a href="#main-content" className="skip-link"> | |
| Skip to content | |
| </a> | |
| <div className="container flex items-center justify-between py-4"> | |
| <Link to="/" className="flex items-center gap-3" aria-label="Jade Infra home"> | |
| <ImageFlex | |
| base="/assets/logo" | |
| alt="Jade Infra logo" | |
| className="h-12 md:h-14 w-auto -my-2 md:-my-3" | |
| /> | |
| <span className="sr-only">Jade Infra</span> | |
| </Link> | |
| <nav aria-label="Primary" className="hidden items-center gap-8 md:flex"> | |
| {navLink('/', 'Home')} | |
| {navLink('/about', 'About')} | |
| {/* Projects with dropdown (hover with grace period) */} | |
| <div | |
| className="relative" | |
| onMouseEnter={() => { | |
| if (closeTimerRef.current) clearTimeout(closeTimerRef.current); | |
| setProjectsOpen(true); | |
| }} | |
| onMouseLeave={() => { | |
| if (closeTimerRef.current) clearTimeout(closeTimerRef.current); | |
| closeTimerRef.current = setTimeout(() => setProjectsOpen(false), 250); | |
| }} | |
| > | |
| <NavLink | |
| to="/projects/development" | |
| className={({ isActive }) => | |
| clsx( | |
| 'px-3 py-2 text-sm font-medium transition', | |
| isActive ? 'text-brand-700' : 'text-slate-700 hover:text-brand-700' | |
| ) | |
| } | |
| aria-haspopup="true" | |
| aria-expanded={projectsOpen} | |
| onFocus={() => setProjectsOpen(true)} | |
| > | |
| Projects | |
| </NavLink> | |
| <div | |
| className={clsx( | |
| 'absolute left-0 top-full w-56 border border-slate-200 bg-white p-2 shadow-card', | |
| 'transition-opacity', | |
| projectsOpen ? 'opacity-100 pointer-events-auto mt-2' : 'opacity-0 pointer-events-none mt-2' | |
| )} | |
| role="menu" | |
| aria-label="Projects submenu" | |
| > | |
| <NavLink | |
| to="/projects/development" | |
| className={({ isActive }) => | |
| clsx( | |
| 'relative block rounded px-3 py-2 text-sm transition', | |
| 'after:content-[""] after:absolute after:left-3 after:right-3 after:-bottom-0.5 after:h-[2px] after:bg-brand-600', | |
| 'after:transform after:scale-x-0 after:origin-right after:transition-transform after:duration-300', | |
| 'hover:after:origin-left hover:after:scale-x-100', | |
| isActive ? 'text-brand-700' : 'text-slate-700 hover:text-brand-700' | |
| ) | |
| } | |
| role="menuitem" | |
| > | |
| Development | |
| </NavLink> | |
| <NavLink | |
| to="/projects/construction" | |
| className={({ isActive }) => | |
| clsx( | |
| 'relative block rounded px-3 py-2 text-sm transition', | |
| 'after:content-[""] after:absolute after:left-3 after:right-3 after:-bottom-0.5 after:h-[2px] after:bg-brand-600', | |
| 'after:transform after:scale-x-0 after:origin-right after:transition-transform after:duration-300', | |
| 'hover:after:origin-left hover:after:scale-x-100', | |
| isActive ? 'text-brand-700' : 'text-slate-700 hover:text-brand-700' | |
| ) | |
| } | |
| role="menuitem" | |
| > | |
| Project Contracting | |
| </NavLink> | |
| <NavLink | |
| to="/projects/sra" | |
| className={({ isActive }) => | |
| clsx( | |
| 'relative block rounded px-3 py-2 text-sm transition', | |
| 'after:content-[""] after:absolute after:left-3 after:right-3 after:-bottom-0.5 after:h-[2px] after:bg-brand-600', | |
| 'after:transform after:scale-x-0 after:origin-right after:transition-transform after:duration-300', | |
| 'hover:after:origin-left hover:after:scale-x-100', | |
| isActive ? 'text-brand-700' : 'text-slate-700 hover:text-brand-700' | |
| ) | |
| } | |
| role="menuitem" | |
| > | |
| SRA | |
| </NavLink> | |
| </div> | |
| </div> | |
| {navLink('/projects/redevelopment', 'Redevelopment')} | |
| {navLink('/contact', 'Contact')} | |
| </nav> | |
| <div className="flex items-center gap-3"> | |
| <button | |
| type="button" | |
| aria-controls="mobile-menu" | |
| aria-expanded={menuOpen} | |
| aria-label="Toggle menu" | |
| className="inline-flex h-10 w-10 items-center justify-center rounded md:hidden | |
| focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600" | |
| onClick={() => setMenuOpen((v) => !v)} | |
| > | |
| <svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true"> | |
| <path | |
| d={menuOpen ? 'M6 18L18 6M6 6l12 12' : 'M3 6h18M3 12h18M3 18h18'} | |
| stroke="currentColor" | |
| strokeWidth="2" | |
| strokeLinecap="round" | |
| /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div | |
| id="mobile-menu" | |
| className={clsx( | |
| 'md:hidden', | |
| menuOpen ? 'block' : 'hidden' | |
| )} | |
| > | |
| <div className="container space-y-1 pb-6"> | |
| <NavLink | |
| to="/" | |
| className="block rounded px-4 py-3 text-base hover:bg-slate-50" | |
| onClick={() => setMenuOpen(false)} | |
| > | |
| Home | |
| </NavLink> | |
| <NavLink | |
| to="/about" | |
| className="block rounded px-4 py-3 text-base hover:bg-slate-50" | |
| onClick={() => setMenuOpen(false)} | |
| > | |
| About | |
| </NavLink> | |
| <div> | |
| <NavLink | |
| to="/projects/development" | |
| className="block rounded px-4 py-3 text-base hover:bg-slate-50" | |
| onClick={() => setMenuOpen(false)} | |
| > | |
| Projects | |
| </NavLink> | |
| <div className="ml-4 mt-1 space-y-1"> | |
| <NavLink to="/projects/development" className="block rounded px-4 py-2 text-sm hover:bg-slate-50" onClick={() => setMenuOpen(false)}>Development</NavLink> | |
| <NavLink to="/projects/construction" className="block rounded px-4 py-2 text-sm hover:bg-slate-50" onClick={() => setMenuOpen(false)}>Project Contracting</NavLink> | |
| <NavLink to="/projects/sra" className="block rounded px-4 py-2 text-sm hover:bg-slate-50" onClick={() => setMenuOpen(false)}>SRA</NavLink> | |
| </div> | |
| </div> | |
| <NavLink | |
| to="/projects/redevelopment" | |
| className="block rounded px-4 py-3 text-base hover:bg-slate-50" | |
| onClick={() => setMenuOpen(false)} | |
| > | |
| Redevelopment | |
| </NavLink> | |
| <NavLink | |
| to="/contact" | |
| className="block rounded px-4 py-3 text-base hover:bg-slate-50" | |
| onClick={() => setMenuOpen(false)} | |
| > | |
| Contact | |
| </NavLink> | |
| </div> | |
| </div> | |
| </header> | |
| </> | |
| ); | |
| } |