"use client"; import React, { useState, useEffect, useRef } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import AppImage from "@/components/ui/AppImage"; import { LinkedInIcon, InstagramIcon, 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"; import AdImage from "@/components/AdImage"; import EventsDropdown from "@/components/EventsDropdown"; import AboutDropdown from "@/components/AboutDropdown"; import { ADS_728X90, shuffle } from "@/lib/ads"; interface NavItem { label: string; href: string; dropdown?: { label: string; href: string }[]; } const navLinks: NavItem[] = [ { label: "Newsletter", href: "/newsletter" }, { label: "Forum", href: "/forum" }, ]; export default function Header() { const router = useRouter(); const [scrolled, setScrolled] = useState(false); const [mobileOpen, setMobileOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [searchFocused, setSearchFocused] = useState(false); const [suggestions, setSuggestions] = useState< Array<{ slug: string; title: string; category?: string; date?: string | null; author_name?: string | null; snippet?: { text: string; matchStart: number; matchEnd: number } | null; matched_in?: "title" | "excerpt" | "content" | "author" | null; href: string; }> >([]); const [suggestOpen, setSuggestOpen] = useState(false); const submitSearch = () => { const q = searchQuery.trim(); if (!q) return; setMobileOpen(false); setSuggestOpen(false); router.push(`/search?q=${encodeURIComponent(q)}`); }; // Autocomplete: debounced fetch to /api/search/suggest while the user types. useEffect(() => { const q = searchQuery.trim(); if (q.length < 2) { setSuggestions([]); return; } const handle = setTimeout(() => { fetch(`/api/search/suggest?q=${encodeURIComponent(q)}&limit=8`, { cache: "no-store" }) .then((r) => r.ok ? r.json() : { items: [] }) .then((d) => { setSuggestions(d.items || []); setSuggestOpen(true); }) .catch(() => setSuggestions([])); }, 150); return () => clearTimeout(handle); }, [searchQuery]); const [currentUserId, setCurrentUserId] = useState(null); const [currentUserEmail, setCurrentUserEmail] = useState(null); const [isAdmin, setIsAdmin] = useState(false); const [subEmail, setSubEmail] = useState(""); const [subStatus, setSubStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); const [subMessage, setSubMessage] = useState(""); const searchRef = useRef(null); const mobileMenuRef = useRef(null); const hamburgerRef = useRef(null); const navLinksRef = 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(); const ADMIN_ROLES = new Set([ "administrator", "admin", "super_admin", "editor", "support", ]); const refreshAdmin = async (userId: string | null) => { if (!userId) { setIsAdmin(false); return; } const { data } = await supabase .from("user_profiles") .select("role") .eq("id", userId) .maybeSingle(); setIsAdmin(!!data?.role && ADMIN_ROLES.has(data.role)); }; supabase.auth.getUser().then(({ data: { user } }) => { setCurrentUserId(user?.id || null); setCurrentUserEmail(user?.email || null); refreshAdmin(user?.id || null); }); const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { setCurrentUserId(session?.user?.id || null); setCurrentUserEmail(session?.user?.email || null); refreshAdmin(session?.user?.id || 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( '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 */}

News & Intelligence for Broadcast, Post-Production & Streaming Technology

{/* Language Switcher — Section 18B */}
{/* Sign In / Register removed — PR firms now authenticate at distribution.relevantmediaproperties.com; /wp-login, /wp-admin, /login redirect there from middleware. */} {currentUserId && ( My Profile )} {isAdmin && ( <> WP Import Articles Editorial Newsletter {/* TOOLS dropdown — admin tools consolidated */}
Users Analytics Audit Log Notifications Forum Seed Company Tracker Show Calendar AI Console Banned Terms
Dashboard )} {/* Inline Email Subscription Form */} {subStatus === "success" ? ( {subMessage} ) : (
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" /> {subStatus === "error" && ( {subMessage} )}
)}
{/* MAIN NAV */}
{/* HEADER LEADERBOARD — site-wide 728x90 between main nav and the ticker */} {ADS_728X90.length > 0 && (
)} {/* BROWSE BAR — search left, primary nav right */}
{/* Search (left) — with autocomplete dropdown */}
{suggestOpen && suggestions.length > 0 && (
    {suggestions.map((s) => { const snippet = s.snippet; const beforeMatch = snippet?.text.slice(0, snippet.matchStart) || ""; const matchText = snippet?.text.slice(snippet.matchStart, snippet.matchEnd) || ""; const afterMatch = snippet?.text.slice(snippet.matchEnd) || ""; return (
  • e.preventDefault()} onClick={() => { setSuggestOpen(false); setSearchQuery(""); }}>
    {s.title} {s.category && {s.category}}
    {snippet && s.matched_in !== "title" && (
    {beforeMatch} {matchText} {afterMatch}
    )} {(s.date || s.author_name) && (
    {s.author_name && {s.author_name}} {s.author_name && s.date && ·} {s.date && } {s.matched_in === "author" && match in byline} {s.matched_in === "content" && match in article body}
    )}
  • ); })}
)}
{/* Menu (right) */}
); }