cross-link: forum auto-link, NAB hub, sponsor strip, recent stories
Feature 3 — forum auto-linking
Thread + reply bodies in /forum/thread/[id] now wrap company-name
mentions with /manufacturers/<slug> links via a new client-side
LinkifyPlainText component. Loads the directory list once per page
(module-level promise cache), regex-scans each body, preserves
whitespace, caps at 10 links per body to avoid clutter.
CompanyMentionsHover mounted on the page so the same preview card
pops on hover.
Adds /api/public/companies/list — minimal { name, slug } directory
feed, cached 10min.
Feature 4 — /nab-2026 live coverage hub
New page aggregating every directory company with exhibits_nab=true.
Three sections:
- Coverage Partners — active advertisers exhibiting at NAB, larger
tiles with emerald Sponsor border
- Latest from the show floor — last 60 days of articles with NAB
in the title, cinematic grid
- All NAB 2026 exhibitors — alphabetical, capped at 200 with link
to full /manufacturers directory
Every company tile carries data-company-slug for hover previews.
Feature 5b — Sponsor logo strip on homepage
New SponsorLogoStrip server component renders just above the footer
on the homepage, showing logos of every current banner advertiser
with a directory entry + logo. Each links to the manufacturer
profile with the hover-card slug attribute.
Feature 5c — Recent coverage on manufacturer profiles
/manufacturers/[slug] now includes a grid of the 6 most recent
articles matching the company name in the title, with thumbnails.
Renames the existing manufacturer_id-linked block to "Press
releases" since it's a separate data source.
This commit is contained in:
89
src/components/SponsorLogoStrip.tsx
Normal file
89
src/components/SponsorLogoStrip.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import Link from "next/link";
|
||||
import { createAdminClient } from "@/lib/supabase/admin";
|
||||
|
||||
type Sponsor = {
|
||||
slug: string;
|
||||
name: string;
|
||||
logoUrl: string | null;
|
||||
};
|
||||
|
||||
async function fetchSponsors(): Promise<Sponsor[]> {
|
||||
try {
|
||||
const adv = createAdminClient("adv");
|
||||
const { data: ads } = await adv
|
||||
.from("active_advertisers_by_property")
|
||||
.select("company_name_lower")
|
||||
.eq("property", "broadcastbeat");
|
||||
const names = Array.from(new Set((ads || []).map((r: any) => String(r.company_name_lower))));
|
||||
if (names.length === 0) return [];
|
||||
|
||||
const bb = createAdminClient();
|
||||
const { data: dir } = await bb
|
||||
.from("tracked_companies")
|
||||
.select("slug, company_name, logo_url")
|
||||
.eq("directory_visible", true)
|
||||
.in("company_name", names.map((n) => n)); // case-sensitive — directory should match
|
||||
// Some directory entries may have slightly different casing — fall back
|
||||
// to a case-insensitive single-name lookup for anything missed.
|
||||
const bySlug = new Map<string, Sponsor>();
|
||||
for (const d of (dir || []) as any[]) {
|
||||
bySlug.set(d.slug, { slug: d.slug, name: d.company_name, logoUrl: d.logo_url || null });
|
||||
}
|
||||
const matchedNames = new Set((dir || []).map((d: any) => String(d.company_name).toLowerCase()));
|
||||
const missing = names.filter((n) => !matchedNames.has(n));
|
||||
if (missing.length > 0) {
|
||||
for (const n of missing) {
|
||||
const { data: hit } = await bb
|
||||
.from("tracked_companies")
|
||||
.select("slug, company_name, logo_url")
|
||||
.eq("directory_visible", true)
|
||||
.ilike("company_name", n)
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (hit) bySlug.set(hit.slug, { slug: hit.slug, name: hit.company_name, logoUrl: hit.logo_url || null });
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(bySlug.values()).filter((s) => s.logoUrl); // only show those with a logo
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default async function SponsorLogoStrip() {
|
||||
const sponsors = await fetchSponsors();
|
||||
if (sponsors.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="max-w-container mx-auto px-4 py-6 border-t border-b border-[#1a1a1a]" aria-label="Coverage partners">
|
||||
<div className="flex items-center gap-4 mb-3">
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#3b82f6] font-bold">
|
||||
Coverage Partners
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-[#1a1a1a]" />
|
||||
<Link href="/advertise" className="text-[10px] text-[#666] hover:text-[#3b82f6] font-mono">
|
||||
Advertise →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-4">
|
||||
{sponsors.map((s) => (
|
||||
<Link
|
||||
key={s.slug}
|
||||
href={`/manufacturers/${s.slug}`}
|
||||
data-company-slug={s.slug}
|
||||
className="block opacity-70 hover:opacity-100 transition-opacity"
|
||||
title={s.name}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={s.logoUrl || ""}
|
||||
alt={s.name}
|
||||
className="h-10 md:h-12 w-auto object-contain max-w-[140px] bg-white p-1.5 rounded"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user