initial commit: rocket.new export of broadcastbeat
This commit is contained in:
514
src/components/Header.tsx
Normal file
514
src/components/Header.tsx
Normal file
@@ -0,0 +1,514 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import AppImage from "@/components/ui/AppImage";
|
||||
import {
|
||||
LinkedInIcon,
|
||||
InstagramIcon,
|
||||
TwitterXIcon,
|
||||
FacebookIcon,
|
||||
RssIcon,
|
||||
MenuIcon,
|
||||
CloseIcon,
|
||||
SearchIcon,
|
||||
} from "@/components/ui/Icons";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import NotificationCenter from "@/components/NotificationCenter";
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "NEWS", href: "/news" },
|
||||
{ label: "GEAR & REVIEWS", href: "/gear" },
|
||||
{ label: "SHOW COVERAGE", href: "/show-coverage" },
|
||||
{ label: "TECHNOLOGY", href: "/technology" },
|
||||
{ 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);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchFocused, setSearchFocused] = useState(false);
|
||||
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
|
||||
const [currentUserEmail, setCurrentUserEmail] = useState<string | null>(null);
|
||||
const [subEmail, setSubEmail] = useState("");
|
||||
const [subStatus, setSubStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
||||
const [subMessage, setSubMessage] = useState("");
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
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);
|
||||
window.addEventListener("scroll", handler, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handler);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const supabase = createClient();
|
||||
supabase.auth.getUser().then(({ data: { user } }) => {
|
||||
setCurrentUserId(user?.id || null);
|
||||
setCurrentUserEmail(user?.email || null);
|
||||
});
|
||||
const supabase2 = createClient();
|
||||
const { data: { subscription } } = supabase2.auth.onAuthStateChange((_event, session) => {
|
||||
setCurrentUserId(session?.user?.id || null);
|
||||
setCurrentUserEmail(session?.user?.email || null);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
// Global keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const tag = document.activeElement?.tagName;
|
||||
const isInput = tag === "INPUT" || tag === "TEXTAREA";
|
||||
|
||||
// "/" to focus search
|
||||
if (e.key === "/" && !isInput) {
|
||||
e.preventDefault();
|
||||
searchRef.current?.focus();
|
||||
}
|
||||
|
||||
// Cmd/Ctrl+Shift+A — open AI Console
|
||||
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === "A") {
|
||||
e.preventDefault();
|
||||
window.location.href = "/dashboard/ai-console";
|
||||
}
|
||||
|
||||
// Escape: close mobile menu or clear search
|
||||
if (e.key === "Escape") {
|
||||
if (mobileOpen) {
|
||||
setMobileOpen(false);
|
||||
hamburgerRef.current?.focus();
|
||||
} else if (searchFocused) {
|
||||
setSearchQuery("");
|
||||
searchRef.current?.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [searchFocused, mobileOpen]);
|
||||
|
||||
// Arrow key navigation for desktop nav links
|
||||
const handleNavKeyDown = (e: React.KeyboardEvent, index: number, refs: React.MutableRefObject<(HTMLAnchorElement | null)[]>) => {
|
||||
const items = refs.current.filter(Boolean);
|
||||
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
const next = items[(index + 1) % items.length];
|
||||
next?.focus();
|
||||
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
const prev = items[(index - 1 + items.length) % items.length];
|
||||
prev?.focus();
|
||||
} else if (e.key === "Home") {
|
||||
e.preventDefault();
|
||||
items[0]?.focus();
|
||||
} else if (e.key === "End") {
|
||||
e.preventDefault();
|
||||
items[items.length - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Trap focus inside mobile menu when open
|
||||
useEffect(() => {
|
||||
if (!mobileOpen) return;
|
||||
const menu = mobileMenuRef.current;
|
||||
if (!menu) return;
|
||||
const focusable = menu.querySelectorAll<HTMLElement>(
|
||||
'a[href], button, input, [tabindex]:not([tabIndex="-1"])'
|
||||
);
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
first?.focus();
|
||||
|
||||
const trap = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Tab") return;
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last?.focus();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first?.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
menu.addEventListener("keydown", trap);
|
||||
return () => menu.removeEventListener("keydown", trap);
|
||||
}, [mobileOpen]);
|
||||
|
||||
const handleSubscribe = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!subEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(subEmail)) {
|
||||
setSubStatus("error");
|
||||
setSubMessage("Enter a valid email");
|
||||
return;
|
||||
}
|
||||
setSubStatus("loading");
|
||||
try {
|
||||
const res = await fetch("/api/newsletter/subscribe", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: subEmail, topics: [] }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSubStatus("success");
|
||||
setSubMessage("You're subscribed!");
|
||||
setSubEmail("");
|
||||
setTimeout(() => setSubStatus("idle"), 4000);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setSubStatus("error");
|
||||
setSubMessage(data?.error || "Failed. Try again.");
|
||||
setTimeout(() => setSubStatus("idle"), 3000);
|
||||
}
|
||||
} catch {
|
||||
setSubStatus("error");
|
||||
setSubMessage("Network error. Try again.");
|
||||
setTimeout(() => setSubStatus("idle"), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TOP BAR */}
|
||||
<div className="bg-[#1a2535] border-b border-[#2a3a50]">
|
||||
<div className="max-w-container mx-auto px-4 h-8 flex items-center justify-between">
|
||||
<p className="text-[#999999] text-xs font-body hidden sm:block">
|
||||
Digital Platform for Broadcast Engineering
|
||||
</p>
|
||||
<div className="flex items-center gap-3 ml-auto">
|
||||
<a href="https://linkedin.com" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on LinkedIn"
|
||||
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
|
||||
<LinkedInIcon size={14} />
|
||||
</a>
|
||||
<a href="https://instagram.com" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Instagram"
|
||||
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
|
||||
<InstagramIcon size={14} />
|
||||
</a>
|
||||
<a href="https://twitter.com" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Twitter/X"
|
||||
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
|
||||
<TwitterXIcon size={14} />
|
||||
</a>
|
||||
<a href="https://facebook.com" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Facebook"
|
||||
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
|
||||
<FacebookIcon size={14} />
|
||||
</a>
|
||||
<a href="/rss" aria-label="BroadcastBeat RSS Feed" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
|
||||
<RssIcon size={14} />
|
||||
</a>
|
||||
<div className="w-px h-3.5 bg-[#333] mx-1" />
|
||||
{/* Language Switcher — Section 18B */}
|
||||
<LanguageSwitcher compact />
|
||||
<div className="w-px h-3.5 bg-[#333] mx-1" />
|
||||
<Link href="/account" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
{currentUserId ? "My Account" : "Sign In"}
|
||||
</Link>
|
||||
{!currentUserId && (
|
||||
<Link href="/register" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Register
|
||||
</Link>
|
||||
)}
|
||||
{currentUserId && (
|
||||
<Link href={`/profile/${currentUserId}`} className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
My Profile
|
||||
</Link>
|
||||
)}
|
||||
<Link href="/admin/import" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
WP Import
|
||||
</Link>
|
||||
<Link href="/admin/articles" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Articles
|
||||
</Link>
|
||||
<Link href="/dashboard/editorial" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Editorial
|
||||
</Link>
|
||||
<Link href="/admin/users" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Users
|
||||
</Link>
|
||||
<Link href="/admin/analytics" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Analytics
|
||||
</Link>
|
||||
<Link href="/admin/audit-log" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Audit Log
|
||||
</Link>
|
||||
<Link href="/admin/notifications" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Notifications
|
||||
</Link>
|
||||
<Link href="/admin/newsletter" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Newsletter
|
||||
</Link>
|
||||
<Link href="/admin/forum-seed" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Forum Seed
|
||||
</Link>
|
||||
<Link href="/admin/company-tracker" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Company Tracker
|
||||
</Link>
|
||||
<Link href="/dashboard/show-calendar" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Show Calendar
|
||||
</Link>
|
||||
<Link href="/dashboard/ai-console" className="text-[#3b82f6] hover:text-blue-300 focus:text-blue-300 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
AI Console
|
||||
</Link>
|
||||
<Link href="/admin/banned-terms" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Banned Terms
|
||||
</Link>
|
||||
<Link href="/admin" className="text-[#3b82f6] hover:text-blue-300 focus:text-blue-300 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Dashboard
|
||||
</Link>
|
||||
{/* Inline Email Subscription Form */}
|
||||
{subStatus === "success" ? (
|
||||
<span className="text-[#3b82f6] text-xs font-body font-bold whitespace-nowrap">{subMessage}</span>
|
||||
) : (
|
||||
<form onSubmit={handleSubscribe} className="flex items-center gap-1" aria-label="Newsletter subscription">
|
||||
<input
|
||||
type="email"
|
||||
value={subEmail}
|
||||
onChange={(e) => setSubEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
aria-label="Email address for newsletter"
|
||||
disabled={subStatus === "loading"}
|
||||
className="h-5 px-2 text-[11px] bg-[#0d1520] border border-[#2a3a50] text-[#ccc] placeholder-[#555] rounded focus:outline-none focus:border-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] w-32 disabled:opacity-50 transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={subStatus === "loading"}
|
||||
className="h-5 px-2.5 text-[11px] font-body font-bold uppercase tracking-wider bg-[#3b82f6] hover:bg-[#2563eb] text-white rounded disabled:opacity-50 transition-colors whitespace-nowrap focus:outline-none focus-visible:ring-1 focus-visible:ring-white focus-visible:ring-offset-1 focus-visible:ring-offset-[#3b82f6]"
|
||||
>
|
||||
{subStatus === "loading" ? "..." : "Subscribe"}
|
||||
</button>
|
||||
{subStatus === "error" && (
|
||||
<span className="text-red-400 text-[10px] whitespace-nowrap">{subMessage}</span>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAIN NAV + CATEGORY SUB-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">
|
||||
<div className="flex items-center justify-between h-[60px] gap-4">
|
||||
{/* Logo */}
|
||||
<Link href="/home-page" className="flex-shrink-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] focus-visible:ring-offset-1 focus-visible:ring-offset-[#111]">
|
||||
<AppImage
|
||||
src="https://www.broadcastbeat.com/wp-content/uploads/80077_broadcastbeat_FLAT_A_02-e1658883849688-1024x132.png"
|
||||
alt="BroadcastBeat logo"
|
||||
width={200}
|
||||
height={26}
|
||||
priority={true}
|
||||
className="h-7 w-auto object-contain"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav Links — arrow key navigable */}
|
||||
<div className="hidden lg:flex items-center gap-5" role="list" aria-label="Site sections">
|
||||
{navLinks?.map((link, i) => (
|
||||
<Link
|
||||
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>
|
||||
))}
|
||||
{/* Reading List Icon Link */}
|
||||
<Link
|
||||
href="/reading-list"
|
||||
aria-label="Reading List"
|
||||
className="flex items-center text-[#888] hover:text-[#3b82f6] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] focus-visible:ring-offset-1 focus-visible:ring-offset-[#111]">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</Link>
|
||||
{/* Notification Bell */}
|
||||
<NotificationCenter />
|
||||
</div>
|
||||
|
||||
{/* Enhanced Search */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<div className={`search-bar-wrapper ${searchFocused ? "search-bar-expanded" : ""}`}>
|
||||
<input
|
||||
ref={searchRef}
|
||||
type="search"
|
||||
placeholder={searchFocused ? "Search articles..." : "Search..."}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e?.target?.value)}
|
||||
onFocus={() => setSearchFocused(true)}
|
||||
onBlur={() => setSearchFocused(false)}
|
||||
className="search-input search-input-enhanced pr-16"
|
||||
aria-label="Search articles (press / to focus)"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setSearchQuery(""); searchRef.current?.focus(); }}
|
||||
className="search-clear-btn"
|
||||
aria-label="Clear search">
|
||||
<CloseIcon size={11} />
|
||||
</button>
|
||||
)}
|
||||
{!searchQuery && !searchFocused && (
|
||||
<span className="search-kbd-hint" aria-hidden="true">/</span>
|
||||
)}
|
||||
{(searchQuery || searchFocused) && (
|
||||
<SearchIcon
|
||||
size={13}
|
||||
strokeWidth={1.75}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[#3b82f6] pointer-events-none transition-colors"
|
||||
/>
|
||||
)}
|
||||
{!searchQuery && !searchFocused && (
|
||||
<SearchIcon
|
||||
size={13}
|
||||
strokeWidth={1.75}
|
||||
className="absolute right-8 top-1/2 -translate-y-1/2 text-[#666] pointer-events-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Hamburger */}
|
||||
<button
|
||||
ref={hamburgerRef}
|
||||
className="lg:hidden p-2 text-[#cccccc] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]"
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
aria-label={mobileOpen ? "Close menu" : "Open menu"}
|
||||
aria-expanded={mobileOpen}
|
||||
aria-controls="mobile-menu">
|
||||
{mobileOpen ? <CloseIcon size={22} /> : <MenuIcon size={22} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileOpen && (
|
||||
<div
|
||||
id="mobile-menu"
|
||||
ref={mobileMenuRef}
|
||||
className="lg:hidden mobile-menu-open border-t border-[#252525] pb-4 bg-[#111111]"
|
||||
role="dialog"
|
||||
aria-label="Mobile navigation menu">
|
||||
<div className="flex flex-col gap-0 pt-2">
|
||||
{navLinks?.map((link, i) => (
|
||||
<Link
|
||||
key={link?.label}
|
||||
href={link?.href}
|
||||
ref={(el) => { navLinksRef.current[i] = el; }}
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] 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>
|
||||
))}
|
||||
{/* Reading List mobile link */}
|
||||
<Link
|
||||
href="/reading-list"
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] 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] flex items-center gap-2"
|
||||
onClick={() => setMobileOpen(false)}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
Reading List
|
||||
</Link>
|
||||
{/* Account mobile link */}
|
||||
<Link
|
||||
href="/account"
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] hover:text-[#3b82f6] hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#3b82f6] flex items-center gap-2"
|
||||
onClick={() => setMobileOpen(false)}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
Account
|
||||
</Link>
|
||||
{/* WP Import mobile link */}
|
||||
<Link
|
||||
href="/admin/import"
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] hover:text-[#3b82f6] hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#3b82f6] flex items-center gap-2"
|
||||
onClick={() => setMobileOpen(false)}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
WP Import
|
||||
</Link>
|
||||
</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>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 px-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search articles..."
|
||||
className="search-input search-input-enhanced w-full pr-8"
|
||||
aria-label="Search articles mobile"
|
||||
/>
|
||||
<SearchIcon
|
||||
size={13}
|
||||
strokeWidth={1.75}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[#666] pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</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