Queue-North-Website/src/components/layout/Header.jsx

300 lines
14 KiB
JavaScript

import { useState, useEffect } from 'react'
import { Sheet, SheetTrigger, SheetContent, SheetTitle } from '@/components/ui/Sheet'
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
import { Link, useLocation } from 'react-router-dom'
const Header = () => {
const [isScrolled, setIsScrolled] = useState(false)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [openDropdown, setOpenDropdown] = useState(null)
const location = useLocation()
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
useEffect(() => {
setOpenDropdown(null)
}, [location.pathname])
const navLinks = [
{ name: 'Home', href: '/' },
{ name: 'Services', href: '/services' },
{ name: 'Industries', href: '/industries' },
{ name: 'About', href: '/about' },
{ name: 'Contact', href: '/contact' },
{ name: 'Support', href: '/support' },
]
const serviceLinks = [
{ name: 'Unified Communications', href: '/services/unified-communications' },
{ name: 'Contact Center', href: '/services/contact-center' },
{ name: 'Managed Support', href: '/services/managed-support' },
{ name: 'Consulting & Training', href: '/services/consulting-training' },
{ name: 'Infrastructure Cabling', href: '/services/infrastructure-cabling' },
{ name: 'Wireless Access', href: '/services/wireless-access' },
{ name: 'Local Networking', href: '/services/local-networking' },
]
const industryLinks = [
{ name: 'Healthcare', href: '/industries/healthcare' },
{ name: 'Retail', href: '/industries/retail' },
{ name: 'Manufacturing', href: '/industries/manufacturing' },
{ name: 'Education & Finance', href: '/industries/education-finance' },
]
const closeMobileMenu = () => setMobileMenuOpen(false)
const closeDropdown = () => setOpenDropdown(null)
const isActive = (href) => location.pathname === href
return (
<header className={`sticky top-0 z-40 w-full transition-all duration-300 ${isScrolled ? 'bg-primary-navy shadow-md' : 'bg-primary-navy/95'}`}>
<div className="container mx-auto px-4">
<div className="flex h-16 md:h-18 items-center justify-between">
{/* Logo */}
<div className="flex items-center gap-3">
<Link to="/" className="flex items-center gap-3" aria-label="Queue North Technologies Home">
<img
src="/logo.png"
alt="Queue North Technologies"
className="brand-logo-on-dark h-12 md:h-16 w-auto flex-shrink-0"
loading="eager"
decoding="async"
width="200"
height="200"
/>
<span className="font-bold text-sm sm:text-xl xl:text-2xl text-white whitespace-nowrap tracking-tight">Queue North Technologies</span>
</Link>
</div>
{/* Desktop Nav */}
{/*
The desktop row starts at lg, not md. Tightening the gaps at md (#214)
did not fix iPad portrait, it only hid the overflow better: measured at
a true 768px the three items want 787px of their natural width against
736px of container, so flex shrank the CTA to 114px, wrapped "Request
Consultation" inside it, and still pushed it 25px past the viewport,
where body{overflow-x:hidden} sliced it off with no scrollbar. That
gap-4 was verified in a desktop window whose scrollbar left ~753px, so
md never engaged and the fix was never actually exercised.
768 to 1023 gets the menu button instead, which is the better tablet
experience anyway: 44px rows rather than 17px ones, and the Services
and Industries submenus are reachable, where the desktop dropdowns
open on hover and a touch device has no hover.
The wordmark stays at text-xl until xl. At exactly 1024 the row was
988px inside 992px of container: four pixels, which a font fallback
or one more nav item would eat. At text-xl it has room.
*/}
<nav className="hidden lg:flex items-center gap-5 xl:gap-6" aria-label="Main navigation">
{navLinks.map((link) => {
const hasDropdown = link.name === 'Services' || link.name === 'Industries'
return (
<div
key={link.name}
className="relative"
onMouseEnter={() => hasDropdown && setOpenDropdown(link.name)}
onMouseLeave={closeDropdown}
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) {
closeDropdown()
}
}}
onKeyDown={(event) => {
if (event.key === 'Escape') {
closeDropdown()
}
}}
>
<Link
to={link.href}
onFocus={() => hasDropdown && setOpenDropdown(link.name)}
onClick={closeDropdown}
className={`block py-3 text-sm font-medium transition-colors ${isActive(link.href) ? 'text-white underline underline-offset-4' : 'text-white/70 hover:text-white'}`}
>
{link.name}
</Link>
{/* Services Dropdown */}
{link.name === 'Services' && (
<div className={`absolute top-full left-0 w-64 bg-white rounded-md shadow-xl border border-gray-200 pt-2 ${openDropdown === 'Services' ? 'block' : 'hidden'}`}>
<div className="p-2">
{serviceLinks.map((service) => (
<Link
key={service.name}
to={service.href}
onClick={closeDropdown}
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
>
{service.name}
</Link>
))}
</div>
</div>
)}
{/* Industries Dropdown */}
{link.name === 'Industries' && (
<div className={`absolute top-full left-0 w-64 bg-white rounded-md shadow-xl border border-gray-200 pt-2 ${openDropdown === 'Industries' ? 'block' : 'hidden'}`}>
<div className="p-2">
{industryLinks.map((industry) => (
<Link
key={industry.name}
to={industry.href}
onClick={closeDropdown}
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
>
{industry.name}
</Link>
))}
</div>
</div>
)}
</div>
)
})}
</nav>
{/* CTA Button */}
{/* shrink-0 and whitespace-nowrap so a row that no longer fits overflows
visibly and the device sweep catches it, instead of flex quietly
squeezing the button and wrapping its label to keep the total inside
a viewport it is already past. */}
<div className="hidden lg:block shrink-0">
<Link to="/contact#contact-form" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-9 px-3 bg-primary-cyan text-primary-navy hover:bg-cyan-600 transition-colors">
Request Consultation
</Link>
</div>
{/* Mobile Menu */}
<div className="lg:hidden">
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
<SheetTrigger asChild>
{/* p-2.5, so the 24px icon sits in a 44x44 target. It is now the
only navigation up to 1023px, and 40x40 was under the mark. */}
<button className="p-2.5 text-white hover:text-primary-cyan transition-colors focus:outline-none focus:ring-2 focus:ring-primary-cyan rounded-md" aria-label="Open navigation menu">
<span className="sr-only">Open menu</span>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</SheetTrigger>
<SheetContent side="right" aria-describedby={undefined} className="w-[85vw] max-w-[300px] sm:w-[350px] bg-primary-navy text-white">
<VisuallyHidden.Root asChild>
<SheetTitle>Navigation Menu</SheetTitle>
</VisuallyHidden.Root>
{/* Logo + phone */}
<div className="flex items-center gap-3 pb-4 border-b border-white/10">
<Link to="/" onClick={closeMobileMenu} aria-label="Queue North Technologies Home" className="flex-shrink-0">
<img src="/logo.png" alt="Queue North Technologies" className="brand-logo-on-dark h-12 w-auto"
loading="lazy"
decoding="async"
width="200"
height="200"
/>
</Link>
<Link to="/" onClick={closeMobileMenu} className="font-bold text-sm tracking-tight whitespace-nowrap text-white leading-tight">
Queue North Technologies
</Link>
</div>
{/* Scrollable nav */}
<nav className="flex-1 min-h-0 overflow-y-auto" aria-label="Mobile navigation">
{/* Main links — Services and Industries handled as sections below */}
<ul className="space-y-1 mb-6">
{navLinks
.filter(link => link.name !== 'Services' && link.name !== 'Industries')
.map((link) => (
<li key={link.name}>
<Link
to={link.href}
onClick={closeMobileMenu}
className={`flex items-center py-3 px-2 rounded-md text-base font-medium transition-colors ${isActive(link.href) ? 'text-white bg-white/10' : 'text-white/70 hover:text-white hover:bg-white/5'}`}
>
{link.name}
</Link>
</li>
))}
</ul>
{/* Services section */}
<div className="mb-6">
<Link
to="/services"
onClick={closeMobileMenu}
className={`block py-2 px-2 text-xs font-semibold uppercase tracking-wider mb-1 transition-colors ${isActive('/services') ? 'text-primary-cyan' : 'text-primary-cyan/80 hover:text-primary-cyan'}`}
>
Services
</Link>
<ul className="space-y-1">
{serviceLinks.map((service) => (
<li key={service.name}>
<Link
to={service.href}
onClick={closeMobileMenu}
className={`flex items-center py-3 px-4 rounded-md text-sm transition-colors ${isActive(service.href) ? 'text-white font-semibold bg-white/10' : 'text-white/70 hover:text-white hover:bg-white/5'}`}
aria-label={service.name}
>
{service.name}
</Link>
</li>
))}
</ul>
</div>
{/* Industries section */}
<div>
<Link
to="/industries"
onClick={closeMobileMenu}
className={`block py-2 px-2 text-xs font-semibold uppercase tracking-wider mb-1 transition-colors ${isActive('/industries') ? 'text-primary-cyan' : 'text-primary-cyan/80 hover:text-primary-cyan'}`}
>
Industries
</Link>
<ul className="space-y-1">
{industryLinks.map((industry) => (
<li key={industry.name}>
<Link
to={industry.href}
onClick={closeMobileMenu}
className={`flex items-center py-3 px-4 rounded-md text-sm transition-colors ${isActive(industry.href) ? 'text-white font-semibold bg-white/10' : 'text-white/70 hover:text-white hover:bg-white/5'}`}
aria-label={industry.name}
>
{industry.name}
</Link>
</li>
))}
</ul>
</div>
</nav>
{/* CTA */}
<div className="pt-4 border-t border-white/10">
<Link
to="/contact#contact-form"
onClick={closeMobileMenu}
className="inline-flex items-center justify-center gap-2 w-full rounded-md text-sm font-semibold h-11 px-4 bg-primary-cyan text-primary-navy hover:bg-white transition-colors duration-200"
aria-label="Get a free quote"
>
Get a Free Quote
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</Link>
</div>
</SheetContent>
</Sheet>
</div>
</div>
</div>
</header>
)
}
export default Header