cross-link: companies-in-this-story sidebar + unified company search
Feature 1 — sidebar
Adds a "Companies in this story" panel on every news article that
surfaces every directory company my linker matched in the body. Each
tile shows logo, name, Sponsor / NAB 26 / IBC badges, and a one-sentence
bio. Tiles link to /manufacturers/<slug> and carry data-company-slug so
the global hover card pops on hover too.
Extends company-mentions.ts with linkifyAndExtractMentions() that
returns both the linked HTML and the ordered list of matched slugs in
one pass, so /news/[slug] doesn't re-scan the body for the sidebar.
Feature 2 — unified typeahead
/api/search/suggest now returns { companies, items } — companies are
matched against bb.tracked_companies (directory_visible=true), capped
at 6, ordered by featured → mention_count, then bumped if they're an
active advertiser on broadcastbeat (adv schema lookup).
Header search dropdown renders a "Companies" section at the top with
logo, name, Sponsor badge (active advertiser) or NAB 26 badge, above
the existing "Stories" results. Each company link carries
data-company-slug so the hover card works in the dropdown too.
This commit is contained in:
@@ -51,6 +51,17 @@ export default function Header() {
|
||||
href: string;
|
||||
}>
|
||||
>([]);
|
||||
const [suggestedCompanies, setSuggestedCompanies] = useState<
|
||||
Array<{
|
||||
slug: string;
|
||||
name: string;
|
||||
logoUrl: string | null;
|
||||
exhibitsNab: boolean;
|
||||
exhibitsIbc: boolean;
|
||||
isSponsor: boolean;
|
||||
href: string;
|
||||
}>
|
||||
>([]);
|
||||
const [suggestOpen, setSuggestOpen] = useState(false);
|
||||
|
||||
const submitSearch = () => {
|
||||
@@ -70,12 +81,13 @@ export default function Header() {
|
||||
}
|
||||
const handle = setTimeout(() => {
|
||||
fetch(`/api/search/suggest?q=${encodeURIComponent(q)}&limit=8`, { cache: "no-store" })
|
||||
.then((r) => r.ok ? r.json() : { items: [] })
|
||||
.then((r) => r.ok ? r.json() : { items: [], companies: [] })
|
||||
.then((d) => {
|
||||
setSuggestions(d.items || []);
|
||||
setSuggestedCompanies(d.companies || []);
|
||||
setSuggestOpen(true);
|
||||
})
|
||||
.catch(() => setSuggestions([]));
|
||||
.catch(() => { setSuggestions([]); setSuggestedCompanies([]); });
|
||||
}, 150);
|
||||
return () => clearTimeout(handle);
|
||||
}, [searchQuery]);
|
||||
@@ -573,11 +585,49 @@ export default function Header() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{suggestOpen && suggestions.length > 0 && (
|
||||
{suggestOpen && (suggestions.length > 0 || suggestedCompanies.length > 0) && (
|
||||
<ul
|
||||
id="bb-search-suggest"
|
||||
role="listbox"
|
||||
className="bb-browse-search-suggest">
|
||||
{suggestedCompanies.length > 0 && (
|
||||
<>
|
||||
<li className="px-3 pt-2 pb-1 text-[10px] uppercase tracking-widest text-[#666] font-bold border-b border-[#1a1a1a]">
|
||||
Companies
|
||||
</li>
|
||||
{suggestedCompanies.map((c) => (
|
||||
<li key={`co-${c.slug}`} role="option">
|
||||
<Link
|
||||
href={c.href}
|
||||
data-company-slug={c.slug}
|
||||
className="bb-browse-search-suggest-item flex items-center gap-2.5"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => { setSuggestOpen(false); setSearchQuery(""); }}>
|
||||
{c.logoUrl ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={c.logoUrl} alt="" className="w-7 h-7 rounded bg-white p-0.5 object-contain flex-shrink-0" loading="lazy" />
|
||||
) : (
|
||||
<div className="w-7 h-7 rounded bg-[#222] flex items-center justify-center text-[#666] font-bold text-xs flex-shrink-0">
|
||||
{(c.name || "?").trim().charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="flex-1 min-w-0 truncate font-semibold text-[#e0e0e0]">{c.name}</span>
|
||||
{c.isSponsor && (
|
||||
<span className="text-[9px] font-bold uppercase tracking-wider bg-emerald-500/15 text-emerald-300 px-1.5 py-0.5 rounded">Sponsor</span>
|
||||
)}
|
||||
{c.exhibitsNab && !c.isSponsor && (
|
||||
<span className="text-[9px] font-bold uppercase tracking-wider bg-amber-500/15 text-amber-300 px-1.5 py-0.5 rounded">NAB 26</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
{suggestions.length > 0 && (
|
||||
<li className="px-3 pt-2 pb-1 text-[10px] uppercase tracking-widest text-[#666] font-bold border-y border-[#1a1a1a]">
|
||||
Stories
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{suggestions.map((s) => {
|
||||
const snippet = s.snippet;
|
||||
const beforeMatch = snippet?.text.slice(0, snippet.matchStart) || "";
|
||||
|
||||
Reference in New Issue
Block a user