ux: dark-theme conversion + forum hero + ticker cleanup

- /manufacturers index + DirectoryClient: convert white/light-ink theme
  to BB dark theme so it stops standing out against the rest of the site
- /manufacturers/[slug]: same conversion, includes the new executives
  card grid and Headquarters block
- /nab-2026: same conversion, including hero gradient and sponsor tile
  borders (amber on dark instead of amber on white)
- Forum: remove "Top Performing Threads" widget from both /forum and
  /forum/[slug] (display only — TopThreadsWidget component preserved
  in case admins want to re-enable later)
- Forum hero: vertical padding py-10 → py-5, tagline reduced from
  three sentences to one. Adds a "Sign in to post" or "Start a thread"
  CTA in the hero's top-right depending on auth state.
- Recent Forum Posts ticker: drop replies entirely, surface only thread
  titles so breadth of conversation shows instead of the loudest
  threads dominating with reply chatter.
This commit is contained in:
Ryan Salazar
2026-05-27 14:52:08 +00:00
parent 52a6792c9e
commit 3460baa8b9
7 changed files with 147 additions and 159 deletions

View File

@@ -22,65 +22,32 @@ export async function GET() {
auth: { persistSession: false }, auth: { persistSession: false },
}); });
// Newest threads + newest replies, interleaved by timestamp. // Threads only — per editorial direction we surface unique topics, not
const [threadsRes, repliesRes] = await Promise.all([ // reply chatter, so the ticker shows breadth of discussion.
sb const threadsRes = await sb
.from('forum_threads') .from('forum_threads')
.select('id, title, author_name, created_at') .select('id, title, author_name, created_at')
.order('created_at', { ascending: false }) .order('created_at', { ascending: false })
.limit(20), .limit(40);
sb
.from('forum_replies')
.select('id, body, thread_id, author_name, created_at, forum_threads(title)')
.order('created_at', { ascending: false })
.limit(20),
]);
if (threadsRes.error && repliesRes.error) { if (threadsRes.error) {
return NextResponse.json( return NextResponse.json(
{ error: threadsRes.error?.message || repliesRes.error?.message, items: [] }, { error: threadsRes.error.message, items: [] },
{ status: 500 } { status: 500 },
); );
} }
const items: TickerItem[] = []; const items: TickerItem[] = (threadsRes.data || []).map((t: any) => ({
for (const t of threadsRes.data || []) {
items.push({
id: t.id, id: t.id,
href: `/forum/thread/${t.id}`, href: `/forum/thread/${t.id}`,
title: t.title, title: t.title,
kind: 'thread', kind: 'thread',
author_name: t.author_name, author_name: t.author_name,
created_at: t.created_at, created_at: t.created_at,
}); }));
}
for (const r of (repliesRes.data || []) as Array<{
id: string;
body: string;
thread_id: string;
author_name: string | null;
created_at: string;
forum_threads?: { title: string } | { title: string }[] | null;
}>) {
const parent =
Array.isArray(r.forum_threads) ? r.forum_threads[0] : r.forum_threads;
const parentTitle = parent?.title || 'forum reply';
items.push({
id: r.id,
href: `/forum/thread/${r.thread_id}`,
title: `Re: ${parentTitle}`,
kind: 'reply',
author_name: r.author_name,
created_at: r.created_at,
});
}
items.sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
return NextResponse.json( return NextResponse.json(
{ items: items.slice(0, 30) }, { items: items.slice(0, 40) },
{ {
headers: { headers: {
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600', 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',

View File

@@ -361,9 +361,6 @@ export default function ForumCategoryPage() {
</form> </form>
)} )}
{/* Top Performing Threads Widget (shown when category is loaded) */}
{!loading && category && <TopThreadsWidget categoryId={category.id} />}
{/* Search + Sort Bar */} {/* Search + Sort Bar */}
<div className="flex flex-col sm:flex-row gap-3 mb-5"> <div className="flex flex-col sm:flex-row gap-3 mb-5">
{/* Search */} {/* Search */}

View File

@@ -264,15 +264,42 @@ export default function ForumIndexPage() {
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#111111]">
{/* Hero — width-aligned with content below + featured-box pattern */} {/* Hero — width-aligned with content below + featured-box pattern */}
<div className="max-w-container mx-auto px-4"> <div className="max-w-container mx-auto px-4">
<div className="bg-[#1a2535] border-b border-[#2a3a50] py-10 px-4"> <div className="bg-[#1a2535] border-b border-[#2a3a50] py-5 px-4">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-start justify-between gap-4 flex-wrap">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-1">
<img src="/logos/crew-lounge-logo.svg" alt="The Crew Lounge" className="w-8 h-8" /> <img src="/logos/crew-lounge-logo.svg" alt="The Crew Lounge" className="w-8 h-8" />
<h1 className="text-3xl font-heading font-bold text-white">The Crew Lounge</h1> <h1 className="text-3xl font-heading font-bold text-white">The Crew Lounge</h1>
</div> </div>
<p className="text-[#aab4c4] font-body text-base max-w-2xl"> <p className="text-[#aab4c4] font-body text-sm max-w-2xl">
The Crew Lounge: connect with broadcast, motion picture and post production professionals worldwide. Ask questions, share expertise, and discuss the technology shaping the industry. Connect with broadcast, motion picture, and post production pros worldwide.
</p> </p>
</div> </div>
<div className="flex-shrink-0">
{currentUserId ? (
<Link
href="/forum/new"
className="inline-flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M12 5v14M5 12h14" />
</svg>
Start a thread
</Link>
) : (
<Link
href="/forum/login?next=/forum"
className="inline-flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3" />
</svg>
Sign in to post
</Link>
)}
</div>
</div>
</div>
</div> </div>
<div className="max-w-container mx-auto px-4 py-6"> <div className="max-w-container mx-auto px-4 py-6">
@@ -505,12 +532,9 @@ export default function ForumIndexPage() {
</div> </div>
)} )}
{/* Default view: Top Threads + Category Grid */} {/* Default view: Category Grid */}
{!isSearchMode && ( {!isSearchMode && (
<> <>
{/* Top Performing Threads Widget */}
{!loading && <TopThreadsWidget />}
{loading && ( {loading && (
<div className="space-y-3 mb-8"> <div className="space-y-3 mb-8">
{[...Array(8)].map((_, i) => ( {[...Array(8)].map((_, i) => (

View File

@@ -53,24 +53,24 @@ export default function ManufacturerDirectoryClient({
return ( return (
<div> <div>
<div className="bg-surface-soft border border-ink/10 rounded-xl p-4 mb-6 flex flex-col gap-3 md:flex-row md:items-center md:gap-4"> <div className="bg-[#111] border border-[#252525] rounded-xl p-4 mb-6 flex flex-col gap-3 md:flex-row md:items-center md:gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<input <input
type="search" type="search"
value={q} value={q}
onChange={(e) => setQ(e.target.value)} onChange={(e) => setQ(e.target.value)}
placeholder="Search by company or product…" placeholder="Search by company or product…"
className="w-full bg-white border border-ink/15 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-primary/40" className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#3b82f6]/40"
/> />
</div> </div>
<div className="flex gap-1 bg-white border border-ink/10 rounded-md p-1"> <div className="flex gap-1 bg-[#0d0d0d] border border-[#252525] rounded-md p-1">
{SHOW_FILTERS.map((f) => ( {SHOW_FILTERS.map((f) => (
<button <button
key={f} key={f}
type="button" type="button"
onClick={() => setShowFilter(f)} onClick={() => setShowFilter(f)}
className={`px-3 py-1.5 rounded text-xs font-semibold ${ className={`px-3 py-1.5 rounded text-xs font-semibold ${
showFilter === f ? "bg-brand-primary text-white" : "text-ink/70 hover:bg-ink/5" showFilter === f ? "bg-[#3b82f6] text-white" : "text-[#aaa] hover:bg-[#1a1a1a]"
}`} }`}
> >
{f} {f}
@@ -87,8 +87,8 @@ export default function ManufacturerDirectoryClient({
onClick={() => setLetter(l)} onClick={() => setLetter(l)}
className={`min-w-[28px] h-7 px-1.5 rounded font-semibold ${ className={`min-w-[28px] h-7 px-1.5 rounded font-semibold ${
letter === l letter === l
? "bg-brand-primary text-white" ? "bg-[#3b82f6] text-white"
: "bg-white border border-ink/10 text-ink/65 hover:bg-ink/5" : "bg-[#0d0d0d] border border-[#252525] text-[#aaa] hover:bg-[#1a1a1a]"
}`} }`}
> >
{l} {l}
@@ -96,7 +96,7 @@ export default function ManufacturerDirectoryClient({
))} ))}
</nav> </nav>
<p className="text-xs text-ink/55 mb-3"> <p className="text-xs text-[#888] mb-3">
Showing {filtered.length.toLocaleString()} of {manufacturers.length.toLocaleString()} manufacturers Showing {filtered.length.toLocaleString()} of {manufacturers.length.toLocaleString()} manufacturers
</p> </p>
@@ -105,9 +105,9 @@ export default function ManufacturerDirectoryClient({
<Link <Link
key={m.slug} key={m.slug}
href={`/manufacturers/${m.slug}`} href={`/manufacturers/${m.slug}`}
className="group bg-white border border-ink/10 rounded-xl p-4 hover:border-brand-primary hover:shadow-sm transition-all flex gap-3" className="group bg-[#0d0d0d] border border-[#252525] rounded-xl p-4 hover:border-[#3b82f6] hover:shadow-sm transition-all flex gap-3"
> >
<div className="w-14 h-14 rounded-lg bg-ink/5 flex items-center justify-center overflow-hidden flex-shrink-0"> <div className="w-14 h-14 rounded-lg bg-[#1a1a1a] flex items-center justify-center overflow-hidden flex-shrink-0">
{m.logo_url ? ( {m.logo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img <img
@@ -117,13 +117,13 @@ export default function ManufacturerDirectoryClient({
loading="lazy" loading="lazy"
/> />
) : ( ) : (
<span className="text-ink/40 text-lg font-semibold"> <span className="text-[#666] text-lg font-semibold">
{(m.company_name || "?").trim().charAt(0).toUpperCase()} {(m.company_name || "?").trim().charAt(0).toUpperCase()}
</span> </span>
)} )}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="font-semibold text-ink truncate group-hover:text-brand-primary transition-colors"> <h3 className="font-semibold text-[#e0e0e0] truncate group-hover:text-[#3b82f6] transition-colors">
{m.company_name} {m.company_name}
</h3> </h3>
<div className="flex items-center gap-1.5 mt-1 mb-1.5 flex-wrap"> <div className="flex items-center gap-1.5 mt-1 mb-1.5 flex-wrap">
@@ -138,12 +138,12 @@ export default function ManufacturerDirectoryClient({
</span> </span>
)} )}
{m.featured && ( {m.featured && (
<span className="text-[10px] font-semibold uppercase tracking-wider bg-brand-primary/10 text-brand-primary px-1.5 py-0.5 rounded"> <span className="text-[10px] font-semibold uppercase tracking-wider bg-[#3b82f6]/15 text-[#3b82f6] px-1.5 py-0.5 rounded">
Featured Featured
</span> </span>
)} )}
</div> </div>
<p className="text-xs text-ink/60 line-clamp-2"> <p className="text-xs text-[#888] line-clamp-2">
{m.bio || "Profile coming soon."} {m.bio || "Profile coming soon."}
</p> </p>
</div> </div>
@@ -152,7 +152,7 @@ export default function ManufacturerDirectoryClient({
</div> </div>
{filtered.length === 0 && ( {filtered.length === 0 && (
<div className="text-center py-12 text-ink/50 text-sm"> <div className="text-center py-12 text-[#888] text-sm">
No manufacturers match these filters. No manufacturers match these filters.
</div> </div>
)} )}

View File

@@ -128,21 +128,21 @@ export default async function ManufacturerProfile({
}; };
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-[#0d0d0d]">
<Header /> <Header />
<script <script
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/> />
<article className="max-w-4xl mx-auto px-4 py-10"> <article className="max-w-4xl mx-auto px-4 py-10">
<nav className="text-xs text-ink/55 mb-6"> <nav className="text-xs text-[#888] mb-6">
<Link href="/manufacturers" className="hover:text-brand-primary"> <Link href="/manufacturers" className="hover:text-[#3b82f6]">
Manufacturer Directory Manufacturer Directory
</Link> </Link>
</nav> </nav>
<header className="flex flex-col sm:flex-row gap-6 mb-8 pb-8 border-b border-ink/10"> <header className="flex flex-col sm:flex-row gap-6 mb-8 pb-8 border-b border-[#252525]">
<div className="w-24 h-24 rounded-xl bg-ink/5 flex items-center justify-center overflow-hidden flex-shrink-0"> <div className="w-24 h-24 rounded-xl bg-[#1a1a1a] flex items-center justify-center overflow-hidden flex-shrink-0">
{company.logo_url ? ( {company.logo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img <img
@@ -151,33 +151,33 @@ export default async function ManufacturerProfile({
className="w-full h-full object-contain" className="w-full h-full object-contain"
/> />
) : ( ) : (
<span className="text-ink/40 text-3xl font-bold"> <span className="text-[#666] text-3xl font-bold">
{(company.company_name || "?").trim().charAt(0).toUpperCase()} {(company.company_name || "?").trim().charAt(0).toUpperCase()}
</span> </span>
)} )}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h1 className="text-3xl md:text-4xl font-display font-bold text-ink mb-1"> <h1 className="text-3xl md:text-4xl font-display font-bold text-[#e0e0e0] mb-1">
{company.company_name} {company.company_name}
</h1> </h1>
<div className="flex items-center gap-2 flex-wrap text-xs"> <div className="flex items-center gap-2 flex-wrap text-xs">
{company.exhibits_nab && ( {company.exhibits_nab && (
<span className="bg-amber-50 text-amber-700 px-2 py-0.5 rounded font-semibold uppercase tracking-wider"> <span className="bg-amber-500/15 text-amber-300 px-2 py-0.5 rounded font-semibold uppercase tracking-wider">
NAB Show 2026 NAB Show 2026
</span> </span>
)} )}
{company.exhibits_ibc && ( {company.exhibits_ibc && (
<span className="bg-blue-50 text-blue-700 px-2 py-0.5 rounded font-semibold uppercase tracking-wider"> <span className="bg-blue-500/15 text-blue-300 px-2 py-0.5 rounded font-semibold uppercase tracking-wider">
IBC IBC
</span> </span>
)} )}
{(company.hq_city || company.hq_country) && ( {(company.hq_city || company.hq_country) && (
<span className="text-ink/60"> <span className="text-[#888]">
{[company.hq_city, company.hq_country].filter(Boolean).join(", ")} {[company.hq_city, company.hq_country].filter(Boolean).join(", ")}
</span> </span>
)} )}
{company.phone && ( {company.phone && (
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-ink/60 hover:text-brand-primary"> <a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#888] hover:text-[#3b82f6]">
{company.phone} {company.phone}
</a> </a>
)} )}
@@ -188,7 +188,7 @@ export default async function ManufacturerProfile({
href={company.company_website} href={company.company_website}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-brand-primary hover:underline" className="text-[#3b82f6] hover:underline"
> >
Website Website
</a> </a>
@@ -198,7 +198,7 @@ export default async function ManufacturerProfile({
href={company.press_url} href={company.press_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-brand-primary hover:underline" className="text-[#3b82f6] hover:underline"
> >
Newsroom Newsroom
</a> </a>
@@ -208,7 +208,7 @@ export default async function ManufacturerProfile({
href={company.contact_url} href={company.contact_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-brand-primary hover:underline" className="text-[#3b82f6] hover:underline"
> >
Contact Contact
</a> </a>
@@ -218,7 +218,7 @@ export default async function ManufacturerProfile({
href={company.linkedin_url} href={company.linkedin_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-ink/60 hover:text-brand-primary" className="text-[#888] hover:text-[#3b82f6]"
> >
LinkedIn LinkedIn
</a> </a>
@@ -228,7 +228,7 @@ export default async function ManufacturerProfile({
href={`https://twitter.com/${company.twitter_handle}`} href={`https://twitter.com/${company.twitter_handle}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-ink/60 hover:text-brand-primary" className="text-[#888] hover:text-[#3b82f6]"
> >
X / Twitter X / Twitter
</a> </a>
@@ -239,10 +239,10 @@ export default async function ManufacturerProfile({
{company.bio && ( {company.bio && (
<section className="mb-10"> <section className="mb-10">
<h2 className="text-lg font-display font-bold text-ink mb-2">About</h2> <h2 className="text-lg font-display font-bold text-[#e0e0e0] mb-2">About</h2>
<div className="text-ink/80 leading-relaxed space-y-4 whitespace-pre-line">{company.bio}</div> <div className="text-[#ccc] leading-relaxed space-y-4 whitespace-pre-line">{company.bio}</div>
{company.bio_source && ( {company.bio_source && (
<p className="text-xs text-ink/40 mt-2"> <p className="text-xs text-[#666] mt-2">
Source: {company.bio_source} Source: {company.bio_source}
</p> </p>
)} )}
@@ -251,33 +251,33 @@ export default async function ManufacturerProfile({
{executives.length > 0 && ( {executives.length > 0 && (
<section className="mb-10"> <section className="mb-10">
<h2 className="text-lg font-display font-bold text-ink mb-4">Executive leadership</h2> <h2 className="text-lg font-display font-bold text-[#e0e0e0] mb-4">Executive leadership</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{executives.map((e: any) => ( {executives.map((e: any) => (
<div key={e.id} className="bg-surface-soft border border-ink/10 rounded-lg p-4 flex gap-3"> <div key={e.id} className="bg-[#111] border border-[#252525] rounded-lg p-4 flex gap-3">
{e.photo_url ? ( {e.photo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img <img
src={e.photo_url} src={e.photo_url}
alt={e.name} alt={e.name}
className="w-16 h-16 rounded-full object-cover bg-ink/5 flex-shrink-0" className="w-16 h-16 rounded-full object-cover bg-[#1a1a1a] flex-shrink-0"
loading="lazy" loading="lazy"
/> />
) : ( ) : (
<div className="w-16 h-16 rounded-full bg-ink/5 flex items-center justify-center text-ink/40 font-bold flex-shrink-0"> <div className="w-16 h-16 rounded-full bg-[#1a1a1a] flex items-center justify-center text-[#666] font-bold flex-shrink-0">
{(e.name || "?").trim().charAt(0).toUpperCase()} {(e.name || "?").trim().charAt(0).toUpperCase()}
</div> </div>
)} )}
<div className="min-w-0"> <div className="min-w-0">
<div className="font-semibold text-ink truncate">{e.name}</div> <div className="font-semibold text-[#e0e0e0] truncate">{e.name}</div>
{e.title && <div className="text-xs text-ink/60 mb-1">{e.title}</div>} {e.title && <div className="text-xs text-[#888] mb-1">{e.title}</div>}
{e.bio && <p className="text-xs text-ink/70 line-clamp-3 mb-1">{e.bio}</p>} {e.bio && <p className="text-xs text-[#aaa] line-clamp-3 mb-1">{e.bio}</p>}
{e.linkedin_url && ( {e.linkedin_url && (
<a <a
href={e.linkedin_url} href={e.linkedin_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[11px] text-brand-primary hover:underline" className="text-[11px] text-[#3b82f6] hover:underline"
> >
LinkedIn LinkedIn
</a> </a>
@@ -291,22 +291,22 @@ export default async function ManufacturerProfile({
{(company.hq_address || company.phone || company.contact_email) && ( {(company.hq_address || company.phone || company.contact_email) && (
<section className="mb-10"> <section className="mb-10">
<h2 className="text-lg font-display font-bold text-ink mb-2">Headquarters</h2> <h2 className="text-lg font-display font-bold text-[#e0e0e0] mb-2">Headquarters</h2>
<address className="not-italic text-ink/80 text-sm leading-relaxed"> <address className="not-italic text-[#ccc] text-sm leading-relaxed">
{company.hq_address && <div>{company.hq_address}</div>} {company.hq_address && <div>{company.hq_address}</div>}
{(company.hq_city || company.hq_country) && ( {(company.hq_city || company.hq_country) && (
<div>{[company.hq_city, company.hq_country].filter(Boolean).join(", ")}</div> <div>{[company.hq_city, company.hq_country].filter(Boolean).join(", ")}</div>
)} )}
{company.phone && ( {company.phone && (
<div className="mt-1"> <div className="mt-1">
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-brand-primary hover:underline"> <a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#3b82f6] hover:underline">
{company.phone} {company.phone}
</a> </a>
</div> </div>
)} )}
{company.contact_email && ( {company.contact_email && (
<div> <div>
<a href={`mailto:${company.contact_email}`} className="text-brand-primary hover:underline"> <a href={`mailto:${company.contact_email}`} className="text-[#3b82f6] hover:underline">
{company.contact_email} {company.contact_email}
</a> </a>
</div> </div>
@@ -317,12 +317,12 @@ export default async function ManufacturerProfile({
{company.categories && company.categories.length > 0 && ( {company.categories && company.categories.length > 0 && (
<section className="mb-10"> <section className="mb-10">
<h2 className="text-lg font-display font-bold text-ink mb-2">Categories</h2> <h2 className="text-lg font-display font-bold text-[#e0e0e0] mb-2">Categories</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{company.categories.map((c: string) => ( {company.categories.map((c: string) => (
<span <span
key={c} key={c}
className="text-xs bg-ink/5 text-ink/70 px-2.5 py-1 rounded-full" className="text-xs bg-[#1a1a1a] text-[#aaa] px-2.5 py-1 rounded-full"
> >
{c} {c}
</span> </span>
@@ -333,21 +333,21 @@ export default async function ManufacturerProfile({
{shows.length > 0 && ( {shows.length > 0 && (
<section className="mb-10"> <section className="mb-10">
<h2 className="text-lg font-display font-bold text-ink mb-3"> <h2 className="text-lg font-display font-bold text-[#e0e0e0] mb-3">
Trade-show appearances Trade-show appearances
</h2> </h2>
<ul className="space-y-2"> <ul className="space-y-2">
{shows.map((s) => ( {shows.map((s) => (
<li <li
key={`${s.show}-${s.show_year}-${s.booth || "?"}`} key={`${s.show}-${s.show_year}-${s.booth || "?"}`}
className="bg-surface-soft border border-ink/10 rounded-md px-4 py-3 flex items-center justify-between gap-4" className="bg-[#111] border border-[#252525] rounded-md px-4 py-3 flex items-center justify-between gap-4"
> >
<div className="min-w-0"> <div className="min-w-0">
<span className="font-semibold text-ink"> <span className="font-semibold text-[#e0e0e0]">
{s.show} {s.show_year} {s.show} {s.show_year}
</span> </span>
{s.booth && ( {s.booth && (
<span className="ml-3 text-sm text-ink/60">Booth {s.booth}</span> <span className="ml-3 text-sm text-[#888]">Booth {s.booth}</span>
)} )}
</div> </div>
</li> </li>
@@ -359,8 +359,8 @@ export default async function ManufacturerProfile({
{recentStories.length > 0 && ( {recentStories.length > 0 && (
<section className="mb-10"> <section className="mb-10">
<header className="flex items-baseline justify-between mb-3"> <header className="flex items-baseline justify-between mb-3">
<h2 className="text-lg font-display font-bold text-ink">Recent coverage</h2> <h2 className="text-lg font-display font-bold text-[#e0e0e0]">Recent coverage</h2>
<Link href={`/news?search=${encodeURIComponent(company.company_name)}`} className="text-xs text-brand-primary hover:underline"> <Link href={`/news?search=${encodeURIComponent(company.company_name)}`} className="text-xs text-[#3b82f6] hover:underline">
All stories All stories
</Link> </Link>
</header> </header>
@@ -369,20 +369,20 @@ export default async function ManufacturerProfile({
<Link <Link
key={r.wp_slug} key={r.wp_slug}
href={`/news/${r.wp_slug}`} href={`/news/${r.wp_slug}`}
className="group bg-white border border-ink/10 rounded-md p-3 hover:border-brand-primary hover:shadow-sm transition-all flex gap-3" className="group bg-[#0d0d0d] border border-[#252525] rounded-md p-3 hover:border-[#3b82f6] hover:shadow-sm transition-all flex gap-3"
> >
{r.featured_image ? ( {r.featured_image ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img src={r.featured_image} alt="" className="w-20 h-14 rounded object-cover bg-ink/5 flex-shrink-0" loading="lazy" /> <img src={r.featured_image} alt="" className="w-20 h-14 rounded object-cover bg-[#1a1a1a] flex-shrink-0" loading="lazy" />
) : ( ) : (
<div className="w-20 h-14 rounded bg-ink/5 flex-shrink-0" /> <div className="w-20 h-14 rounded bg-[#1a1a1a] flex-shrink-0" />
)} )}
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-xs font-semibold text-ink leading-snug group-hover:text-brand-primary line-clamp-3"> <h3 className="text-xs font-semibold text-[#e0e0e0] leading-snug group-hover:text-[#3b82f6] line-clamp-3">
{r.title} {r.title}
</h3> </h3>
{r.wp_published_at && ( {r.wp_published_at && (
<p className="text-[10px] text-ink/50 mt-1"> <p className="text-[10px] text-[#888] mt-1">
{new Date(r.wp_published_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })} {new Date(r.wp_published_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
</p> </p>
)} )}
@@ -395,14 +395,14 @@ export default async function ManufacturerProfile({
{pressReleases.length > 0 && ( {pressReleases.length > 0 && (
<section className="mb-10"> <section className="mb-10">
<h2 className="text-lg font-display font-bold text-ink mb-3">Press releases</h2> <h2 className="text-lg font-display font-bold text-[#e0e0e0] mb-3">Press releases</h2>
<ul className="space-y-2"> <ul className="space-y-2">
{pressReleases.map((p) => ( {pressReleases.map((p) => (
<li key={p.slug || p.source_url} className="text-sm"> <li key={p.slug || p.source_url} className="text-sm">
{p.slug ? ( {p.slug ? (
<Link <Link
href={`/news/${p.slug}`} href={`/news/${p.slug}`}
className="text-brand-primary hover:underline" className="text-[#3b82f6] hover:underline"
> >
{p.title} {p.title}
</Link> </Link>
@@ -410,7 +410,7 @@ export default async function ManufacturerProfile({
<span>{p.title}</span> <span>{p.title}</span>
)} )}
{p.published_at && ( {p.published_at && (
<span className="text-ink/50 text-xs ml-2"> <span className="text-[#888] text-xs ml-2">
{new Date(p.published_at).toISOString().slice(0, 10)} {new Date(p.published_at).toISOString().slice(0, 10)}
</span> </span>
)} )}
@@ -420,7 +420,7 @@ export default async function ManufacturerProfile({
</section> </section>
)} )}
<footer className="text-xs text-ink/40 mt-12 pt-6 border-t border-ink/10"> <footer className="text-xs text-[#666] mt-12 pt-6 border-t border-[#252525]">
Listing maintained by Broadcast Beat editorial. Listing maintained by Broadcast Beat editorial.
{company.last_mentioned && ( {company.last_mentioned && (
<> Last mentioned in coverage on {new Date(company.last_mentioned).toISOString().slice(0, 10)}.</> <> Last mentioned in coverage on {new Date(company.last_mentioned).toISOString().slice(0, 10)}.</>

View File

@@ -52,14 +52,14 @@ export default async function ManufacturersIndex() {
const ibcCount = list.filter((m) => m.exhibits_ibc).length; const ibcCount = list.filter((m) => m.exhibits_ibc).length;
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-[#0d0d0d]">
<Header /> <Header />
<div className="max-w-6xl mx-auto px-4 py-10"> <div className="max-w-6xl mx-auto px-4 py-10">
<header className="mb-8"> <header className="mb-8">
<h1 className="text-3xl md:text-4xl font-display font-bold text-ink mb-2"> <h1 className="text-3xl md:text-4xl font-display font-bold text-[#e0e0e0] mb-2">
Manufacturer Directory Manufacturer Directory
</h1> </h1>
<p className="text-ink/70 text-base max-w-3xl"> <p className="text-[#e0e0e0]/70 text-base max-w-3xl">
{list.length.toLocaleString()} broadcast, post-production, and live-production {list.length.toLocaleString()} broadcast, post-production, and live-production
manufacturers manufacturers
{nabCount > 0 && <> · {nabCount.toLocaleString()} exhibiting at NAB Show 2026</>} {nabCount > 0 && <> · {nabCount.toLocaleString()} exhibiting at NAB Show 2026</>}
@@ -74,7 +74,7 @@ export default async function ManufacturersIndex() {
<ManufacturerDirectoryClient manufacturers={list} /> <ManufacturerDirectoryClient manufacturers={list} />
<section className="mt-16 border-t border-ink/10 pt-8 text-sm text-ink/55 leading-relaxed"> <section className="mt-16 border-t border-[#252525] pt-8 text-sm text-[#888] leading-relaxed">
<p> <p>
Sources: 2026 NAB Show exhibitor list (Map Your Show), IBC exhibitor list, Sources: 2026 NAB Show exhibitor list (Map Your Show), IBC exhibitor list,
manufacturer-direct press releases, and reporting tracked on Broadcast Beat. manufacturer-direct press releases, and reporting tracked on Broadcast Beat.
@@ -82,7 +82,7 @@ export default async function ManufacturersIndex() {
</p> </p>
<p className="mt-2"> <p className="mt-2">
Are you a manufacturer and want to update your listing?{" "} Are you a manufacturer and want to update your listing?{" "}
<Link href="/contact" className="text-brand-primary hover:underline"> <Link href="/contact" className="text-[#3b82f6] hover:underline">
Contact our editors Contact our editors
</Link> </Link>
. .

View File

@@ -87,23 +87,23 @@ export default async function NabHub() {
const articles = (rawArticles || []) as ArticleRow[]; const articles = (rawArticles || []) as ArticleRow[];
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-[#0d0d0d]">
<Header /> <Header />
<CompanyMentionsHover /> <CompanyMentionsHover />
{/* Hero */} {/* Hero */}
<section className="bg-gradient-to-br from-amber-50 to-white border-b border-ink/10"> <section className="bg-gradient-to-br from-amber-500/10 to-[#0d0d0d] border-b border-[#252525]">
<div className="max-w-6xl mx-auto px-4 py-10"> <div className="max-w-6xl mx-auto px-4 py-10">
<div className="flex items-baseline gap-3 mb-2"> <div className="flex items-baseline gap-3 mb-2">
<span className="text-[10px] font-bold uppercase tracking-widest bg-amber-100 text-amber-800 px-2 py-0.5 rounded"> <span className="text-[10px] font-bold uppercase tracking-widest bg-amber-500/20 text-amber-300 px-2 py-0.5 rounded">
Live coverage hub Live coverage hub
</span> </span>
<span className="text-xs text-ink/50">April 1116, 2026 · Las Vegas</span> <span className="text-xs text-[#888]">April 1116, 2026 · Las Vegas</span>
</div> </div>
<h1 className="text-3xl md:text-5xl font-display font-bold text-ink mb-3"> <h1 className="text-3xl md:text-5xl font-display font-bold text-[#e0e0e0] mb-3">
NAB Show 2026 NAB Show 2026
</h1> </h1>
<p className="text-ink/70 text-base max-w-3xl"> <p className="text-[#aaa] text-base max-w-3xl">
{exhibitors.length.toLocaleString()} exhibitors confirmed. {exhibitors.length.toLocaleString()} exhibitors confirmed.
{sponsorRows.length > 0 && <> {sponsorRows.length} are Broadcast Beat coverage partners.</>} {sponsorRows.length > 0 && <> {sponsorRows.length} are Broadcast Beat coverage partners.</>}
{" "}Browse the directory, read the latest product news, and find every booth. {" "}Browse the directory, read the latest product news, and find every booth.
@@ -117,10 +117,10 @@ export default async function NabHub() {
{sponsorRows.length > 0 && ( {sponsorRows.length > 0 && (
<section> <section>
<header className="flex items-baseline justify-between mb-4"> <header className="flex items-baseline justify-between mb-4">
<h2 className="text-xl font-display font-bold text-ink"> <h2 className="text-xl font-display font-bold text-[#e0e0e0]">
Coverage Partners at NAB Coverage Partners at NAB
</h2> </h2>
<span className="text-xs text-ink/50">{sponsorRows.length} exhibiting partner{sponsorRows.length === 1 ? "" : "s"}</span> <span className="text-xs text-[#888]">{sponsorRows.length} exhibiting partner{sponsorRows.length === 1 ? "" : "s"}</span>
</header> </header>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{sponsorRows.map((e) => ( {sponsorRows.map((e) => (
@@ -128,18 +128,18 @@ export default async function NabHub() {
key={e.slug} key={e.slug}
href={`/manufacturers/${e.slug}`} href={`/manufacturers/${e.slug}`}
data-company-slug={e.slug} data-company-slug={e.slug}
className="bg-white border-2 border-emerald-300 rounded-lg p-3 hover:border-emerald-500 hover:shadow-md transition-all flex flex-col items-center text-center gap-2" className="bg-[#0d0d0d] border-2 border-emerald-500/40 rounded-lg p-3 hover:border-emerald-500/70 hover:shadow-md transition-all flex flex-col items-center text-center gap-2"
> >
<div className="w-full h-16 flex items-center justify-center bg-ink/5 rounded"> <div className="w-full h-16 flex items-center justify-center bg-[#1a1a1a] rounded">
{e.logo_url ? ( {e.logo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img src={e.logo_url} alt={`${e.company_name} logo`} className="max-h-12 max-w-full object-contain" loading="lazy" /> <img src={e.logo_url} alt={`${e.company_name} logo`} className="max-h-12 max-w-full object-contain" loading="lazy" />
) : ( ) : (
<span className="text-2xl font-bold text-ink/40">{e.company_name.charAt(0).toUpperCase()}</span> <span className="text-2xl font-bold text-[#666]">{e.company_name.charAt(0).toUpperCase()}</span>
)} )}
</div> </div>
<span className="font-semibold text-ink text-xs truncate w-full">{e.company_name}</span> <span className="font-semibold text-[#e0e0e0] text-xs truncate w-full">{e.company_name}</span>
<span className="text-[9px] font-bold uppercase tracking-wider bg-emerald-100 text-emerald-700 px-1.5 py-0.5 rounded">Sponsor</span> <span className="text-[9px] font-bold uppercase tracking-wider bg-emerald-100 text-emerald-300 px-1.5 py-0.5 rounded">Sponsor</span>
</Link> </Link>
))} ))}
</div> </div>
@@ -150,10 +150,10 @@ export default async function NabHub() {
{articles.length > 0 && ( {articles.length > 0 && (
<section> <section>
<header className="flex items-baseline justify-between mb-4"> <header className="flex items-baseline justify-between mb-4">
<h2 className="text-xl font-display font-bold text-ink"> <h2 className="text-xl font-display font-bold text-[#e0e0e0]">
Latest from the show floor Latest from the show floor
</h2> </h2>
<Link href="/news?search=NAB+2026" className="text-xs text-brand-primary hover:underline"> <Link href="/news?search=NAB+2026" className="text-xs text-[#3b82f6] hover:underline">
See all NAB coverage See all NAB coverage
</Link> </Link>
</header> </header>
@@ -162,7 +162,7 @@ export default async function NabHub() {
<Link <Link
key={a.wp_slug} key={a.wp_slug}
href={`/news/${a.wp_slug}`} href={`/news/${a.wp_slug}`}
className="group bg-white border border-ink/10 rounded-lg overflow-hidden hover:border-brand-primary hover:shadow-sm transition-all" className="group bg-[#0d0d0d] border border-[#252525] rounded-lg overflow-hidden hover:border-[#3b82f6] hover:shadow-sm transition-all"
> >
{a.featured_image && ( {a.featured_image && (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
@@ -174,11 +174,11 @@ export default async function NabHub() {
/> />
)} )}
<div className="p-3"> <div className="p-3">
<h3 className="font-semibold text-ink text-sm leading-snug group-hover:text-brand-primary line-clamp-3 mb-1"> <h3 className="font-semibold text-[#e0e0e0] text-sm leading-snug group-hover:text-[#3b82f6] line-clamp-3 mb-1">
{a.title} {a.title}
</h3> </h3>
{a.wp_published_at && ( {a.wp_published_at && (
<p className="text-[10px] text-ink/50"> <p className="text-[10px] text-[#888]">
{new Date(a.wp_published_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })} {new Date(a.wp_published_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
</p> </p>
)} )}
@@ -192,10 +192,10 @@ export default async function NabHub() {
{/* Full exhibitor list */} {/* Full exhibitor list */}
<section> <section>
<header className="flex items-baseline justify-between mb-4"> <header className="flex items-baseline justify-between mb-4">
<h2 className="text-xl font-display font-bold text-ink"> <h2 className="text-xl font-display font-bold text-[#e0e0e0]">
All NAB 2026 exhibitors All NAB 2026 exhibitors
</h2> </h2>
<Link href="/manufacturers" className="text-xs text-brand-primary hover:underline"> <Link href="/manufacturers" className="text-xs text-[#3b82f6] hover:underline">
Full directory Full directory
</Link> </Link>
</header> </header>
@@ -205,24 +205,24 @@ export default async function NabHub() {
key={e.slug} key={e.slug}
href={`/manufacturers/${e.slug}`} href={`/manufacturers/${e.slug}`}
data-company-slug={e.slug} data-company-slug={e.slug}
className="bg-white border border-ink/10 rounded-md p-2.5 hover:border-brand-primary transition-all flex items-center gap-2.5" className="bg-[#0d0d0d] border border-[#252525] rounded-md p-2.5 hover:border-[#3b82f6] transition-all flex items-center gap-2.5"
> >
{e.logo_url ? ( {e.logo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img src={e.logo_url} alt="" className="w-10 h-10 rounded bg-ink/5 object-contain flex-shrink-0" loading="lazy" /> <img src={e.logo_url} alt="" className="w-10 h-10 rounded bg-[#1a1a1a] object-contain flex-shrink-0" loading="lazy" />
) : ( ) : (
<div className="w-10 h-10 rounded bg-ink/5 flex items-center justify-center text-ink/40 font-bold text-sm flex-shrink-0"> <div className="w-10 h-10 rounded bg-[#1a1a1a] flex items-center justify-center text-[#666] font-bold text-sm flex-shrink-0">
{e.company_name.charAt(0).toUpperCase()} {e.company_name.charAt(0).toUpperCase()}
</div> </div>
)} )}
<span className="font-medium text-ink text-xs truncate">{e.company_name}</span> <span className="font-medium text-[#e0e0e0] text-xs truncate">{e.company_name}</span>
</Link> </Link>
))} ))}
</div> </div>
{otherRows.length > 200 && ( {otherRows.length > 200 && (
<p className="text-center text-xs text-ink/50 mt-4"> <p className="text-center text-xs text-[#888] mt-4">
Showing 200 of {otherRows.length.toLocaleString()} exhibitors.{" "} Showing 200 of {otherRows.length.toLocaleString()} exhibitors.{" "}
<Link href="/manufacturers" className="text-brand-primary hover:underline">Browse the full directory </Link> <Link href="/manufacturers" className="text-[#3b82f6] hover:underline">Browse the full directory </Link>
</p> </p>
)} )}
</section> </section>