Header redesign: dropdowns + site-wide leaderboard, drop secondary nav
Reorganize the navigation: - navLinks now carries an optional dropdown[] per section. NEWS / GEAR & REVIEWS / SHOW COVERAGE / TECHNOLOGY get sub-items pulled from the old flat categoryLinks list. - Desktop main nav renders each top-level item as a hover-triggered dropdown (CSS-only via Tailwind group-hover + group-focus-within for keyboard accessibility). aria-haspopup advertises the popup; the panel is role=menu with menuitem children. - Mobile drawer's category list now groups by parent section instead of showing a flat list. - Removed the secondary category sub-nav row that previously sat below the main nav (its content is in the dropdowns now). - Added a site-wide 728x90 ad slot between the top utility bar and the main nav (md+ only). Uses <AdSlot zone='homepage-top'> so the existing ad-rotation pool serves it. Net: 566 → ~600 lines (markup heavier than what was removed because the dropdown panels are inline).
This commit is contained in:
@@ -15,32 +15,57 @@ import {
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import NotificationCenter from "@/components/NotificationCenter";
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher";
|
||||
import AdSlot from "@/components/AdSlot";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "NEWS", href: "/news" },
|
||||
{ label: "GEAR & REVIEWS", href: "/gear" },
|
||||
{ label: "SHOW COVERAGE", href: "/show-coverage" },
|
||||
{ label: "TECHNOLOGY", href: "/technology" },
|
||||
interface NavItem {
|
||||
label: string;
|
||||
href: string;
|
||||
dropdown?: { label: string; href: string }[];
|
||||
}
|
||||
|
||||
const navLinks: NavItem[] = [
|
||||
{
|
||||
label: "NEWS",
|
||||
href: "/news",
|
||||
dropdown: [
|
||||
{ label: "Live Production", href: "/news?category=live-production" },
|
||||
{ label: "IP & Cloud", href: "/news?category=ip-cloud" },
|
||||
{ label: "AI & Automation", href: "/news?category=ai" },
|
||||
{ label: "People", href: "/news?category=people" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "GEAR & REVIEWS",
|
||||
href: "/gear",
|
||||
dropdown: [
|
||||
{ label: "Audio", href: "/gear?category=audio" },
|
||||
{ label: "Cameras", href: "/gear?category=cameras" },
|
||||
{ label: "Reviews", href: "/gear" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "SHOW COVERAGE",
|
||||
href: "/show-coverage",
|
||||
dropdown: [
|
||||
{ label: "NAB Show", href: "/show-coverage" },
|
||||
{ label: "IBC", href: "/show-coverage?event=ibc" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "TECHNOLOGY",
|
||||
href: "/technology",
|
||||
dropdown: [
|
||||
{ label: "Storage & MAM", href: "/technology?category=storage" },
|
||||
{ label: "Streaming", href: "/technology?category=streaming" },
|
||||
{ label: "AI & Automation", href: "/technology?category=ai" },
|
||||
],
|
||||
},
|
||||
{ label: "NEWSLETTER", href: "/newsletter/archive" },
|
||||
{ label: "FORUM", href: "/forum" },
|
||||
{ label: "ADVERTISE", href: "/advertise" },
|
||||
{ label: "ABOUT", href: "/about" },
|
||||
];
|
||||
|
||||
const categoryLinks = [
|
||||
{ label: "Live Production", href: "/news?category=live-production" },
|
||||
{ label: "IP & Cloud", href: "/news?category=ip-cloud" },
|
||||
{ label: "Audio", href: "/gear?category=audio" },
|
||||
{ label: "Cameras", href: "/gear?category=cameras" },
|
||||
{ label: "Storage & MAM", href: "/technology?category=storage" },
|
||||
{ label: "Streaming", href: "/technology?category=streaming" },
|
||||
{ label: "AI & Automation", href: "/technology?category=ai" },
|
||||
{ label: "NAB Show", href: "/show-coverage" },
|
||||
{ label: "IBC", href: "/show-coverage?event=ibc" },
|
||||
{ label: "People", href: "/news?category=people" },
|
||||
{ label: "Reviews", href: "/gear" },
|
||||
];
|
||||
|
||||
export default function Header() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
@@ -56,7 +81,6 @@ export default function Header() {
|
||||
const mobileMenuRef = useRef<HTMLDivElement>(null);
|
||||
const hamburgerRef = useRef<HTMLButtonElement>(null);
|
||||
const navLinksRef = useRef<(HTMLAnchorElement | null)[]>([]);
|
||||
const categoryLinksRef = useRef<(HTMLAnchorElement | null)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => setScrolled(window.scrollY > 60);
|
||||
@@ -336,7 +360,14 @@ export default function Header() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAIN NAV + CATEGORY SUB-NAV */}
|
||||
{/* HEADER LEADERBOARD — site-wide 728x90 between utility bar and main nav */}
|
||||
<div className="bg-[#0a0a0a] border-b border-[#1a1a1a] py-3 hidden md:block">
|
||||
<div className="max-w-container mx-auto px-4 flex justify-center">
|
||||
<AdSlot zone="homepage-top" size="728x90" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAIN NAV */}
|
||||
<div className={`sticky top-0 z-50 transition-shadow duration-200 ${scrolled ? "shadow-nav" : ""}`}>
|
||||
<nav className="bg-[#111111] border-b border-[#252525]" role="navigation" aria-label="Main navigation">
|
||||
<div className="max-w-container mx-auto px-4">
|
||||
@@ -353,19 +384,39 @@ export default function Header() {
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav Links — arrow key navigable */}
|
||||
<div className="hidden lg:flex items-center gap-5" role="list" aria-label="Site sections">
|
||||
{/* Desktop Nav Links — arrow key navigable + dropdowns */}
|
||||
<div className="hidden lg:flex items-stretch gap-5" role="list" aria-label="Site sections">
|
||||
{navLinks?.map((link, i) => (
|
||||
<Link
|
||||
<div
|
||||
key={link?.label}
|
||||
href={link?.href}
|
||||
ref={(el) => { navLinksRef.current[i] = el; }}
|
||||
role="listitem"
|
||||
className="nav-link-bb focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] focus-visible:ring-offset-1 focus-visible:ring-offset-[#111]"
|
||||
onKeyDown={(e) => handleNavKeyDown(e, i, navLinksRef)}
|
||||
tabIndex={i === 0 ? 0 : -1}>
|
||||
{link?.label}
|
||||
</Link>
|
||||
className="relative group/nav"
|
||||
role="listitem">
|
||||
<Link
|
||||
href={link?.href}
|
||||
ref={(el) => { navLinksRef.current[i] = el; }}
|
||||
className="nav-link-bb h-[60px] flex items-center focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] focus-visible:ring-offset-1 focus-visible:ring-offset-[#111]"
|
||||
onKeyDown={(e) => handleNavKeyDown(e, i, navLinksRef)}
|
||||
tabIndex={i === 0 ? 0 : -1}
|
||||
aria-haspopup={link?.dropdown ? "true" : undefined}>
|
||||
{link?.label}
|
||||
</Link>
|
||||
{link?.dropdown && (
|
||||
<div
|
||||
className="absolute left-0 top-full min-w-[200px] bg-[#0d0d0d] border border-[#252525] shadow-xl rounded-sm py-1 opacity-0 invisible translate-y-1 group-hover/nav:opacity-100 group-hover/nav:visible group-hover/nav:translate-y-0 group-focus-within/nav:opacity-100 group-focus-within/nav:visible group-focus-within/nav:translate-y-0 transition-all duration-150 z-50"
|
||||
role="menu"
|
||||
aria-label={`${link.label} categories`}>
|
||||
{link.dropdown.map((sub) => (
|
||||
<Link
|
||||
key={sub.href + sub.label}
|
||||
href={sub.href}
|
||||
role="menuitem"
|
||||
className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#3b82f6] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#3b82f6] uppercase tracking-wider transition-colors">
|
||||
{sub.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{/* Reading List Icon Link */}
|
||||
<Link
|
||||
@@ -512,15 +563,24 @@ export default function Header() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0 border-t border-[#252525] mt-1">
|
||||
{categoryLinks?.map((link) => (
|
||||
<Link
|
||||
key={link?.label}
|
||||
href={link?.href}
|
||||
className="px-2 py-2 text-xs font-body text-[#888] hover:text-accent hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#3b82f6]"
|
||||
onClick={() => setMobileOpen(false)}>
|
||||
{link?.label}
|
||||
</Link>
|
||||
))}
|
||||
{navLinks
|
||||
.filter((n) => n.dropdown && n.dropdown.length > 0)
|
||||
.map((section) => (
|
||||
<div key={section.label} className="border-b border-[#222]">
|
||||
<div className="px-2 py-2 text-[10px] font-body font-bold text-[#3b82f6] uppercase tracking-widest bg-[#0d0d0d]">
|
||||
{section.label}
|
||||
</div>
|
||||
{section.dropdown!.map((link) => (
|
||||
<Link
|
||||
key={link.href + link.label}
|
||||
href={link.href}
|
||||
className="px-4 py-2 text-xs font-body text-[#888] hover:text-accent hover:bg-[#1a1a1a] block transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#3b82f6]"
|
||||
onClick={() => setMobileOpen(false)}>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 px-2">
|
||||
<div className="relative">
|
||||
@@ -541,26 +601,6 @@ export default function Header() {
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* SECONDARY CATEGORY SUB-NAV */}
|
||||
<nav className="bg-[#0d0d0d] border-b border-[#222] hidden lg:block" aria-label="Category navigation">
|
||||
<div className="max-w-container mx-auto px-4">
|
||||
<div className="flex items-center overflow-x-auto scrollbar-none h-8 gap-0" role="menubar">
|
||||
{categoryLinks?.map((link, i) => (
|
||||
<Link
|
||||
key={link?.label}
|
||||
href={link?.href}
|
||||
ref={(el) => { categoryLinksRef.current[i] = el; }}
|
||||
role="menuitem"
|
||||
className="flex-shrink-0 px-3 h-full flex items-center text-[11px] font-body font-bold text-[#888888] uppercase tracking-wide hover:text-[#3b82f6] hover:bg-[#1e1e1e] transition-colors border-r border-[#222] last:border-r-0 whitespace-nowrap focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#3b82f6]"
|
||||
onKeyDown={(e) => handleNavKeyDown(e, i, categoryLinksRef)}
|
||||
tabIndex={i === 0 ? 0 : -1}>
|
||||
{link?.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user