manufacturers directory: dynamic per-show filter buttons

- page.tsx fetches bb.events (confirmed + tentative) and passes shows
  list into DirectoryClient alongside the tracked_companies rows
- DirectoryClient renders an "All shows" + "NAB+IBC" + one chip per
  event in bb.events. Shows attendee count when known; tags "soon" on
  shows without exhibitor mappings yet (ANGA COM, MPTS, BroadcastAsia,
  SET Expo, NAB NY, SATIS, Hamburg Open, CABSAT, etc.)
- Filter logic uses tracked_companies.shows_attending text[] (added in
  migration 0008) instead of the legacy exhibits_nab / exhibits_ibc
  booleans — scales to N shows.

The directory page was previously capped at 1,000 rows (PostgREST
default max_rows). Server-side bumped to 5000 in a paired supabase
env update — directory now renders all 1,366 manufacturers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Salazar
2026-05-28 17:14:13 +00:00
parent e163746d87
commit 52d5029eed
2 changed files with 93 additions and 16 deletions

View File

@@ -11,22 +11,44 @@ type Manufacturer = {
categories: string[] | null;
exhibits_nab: boolean;
exhibits_ibc: boolean;
shows_attending: string[] | null;
mention_count: number;
featured: boolean;
};
const SHOW_FILTERS = ["All", "NAB Show", "IBC", "Both shows"] as const;
type ShowFilter = (typeof SHOW_FILTERS)[number];
type ShowOption = {
slug: string;
short_name: string | null;
name: string;
start_date: string;
end_date: string;
status: string;
};
export default function ManufacturerDirectoryClient({
manufacturers,
shows = [],
}: {
manufacturers: Manufacturer[];
shows?: ShowOption[];
}) {
const [q, setQ] = useState("");
const [showFilter, setShowFilter] = useState<ShowFilter>("All");
// showFilter is now a show slug from bb.events (e.g. 'nab-show-2026') or
// 'all' / 'both-major' (NAB+IBC overlap).
const [showFilter, setShowFilter] = useState<string>("all");
const [letter, setLetter] = useState<string>("All");
// Per-show exhibitor counts for badge display + auto-hiding empty shows.
const showCounts = useMemo(() => {
const counts: Record<string, number> = {};
for (const m of manufacturers) {
for (const s of m.shows_attending || []) {
counts[s] = (counts[s] || 0) + 1;
}
}
return counts;
}, [manufacturers]);
const filtered = useMemo(() => {
const needle = q.trim().toLowerCase();
return manufacturers.filter((m) => {
@@ -34,9 +56,11 @@ export default function ManufacturerDirectoryClient({
const hay = `${m.company_name} ${m.bio || ""}`.toLowerCase();
if (!hay.includes(needle)) return false;
}
if (showFilter === "NAB Show" && !m.exhibits_nab) return false;
if (showFilter === "IBC" && !m.exhibits_ibc) return false;
if (showFilter === "Both shows" && !(m.exhibits_nab && m.exhibits_ibc)) return false;
if (showFilter === "both-major") {
if (!(m.exhibits_nab && m.exhibits_ibc)) return false;
} else if (showFilter !== "all") {
if (!(m.shows_attending || []).includes(showFilter)) return false;
}
if (letter !== "All") {
const c = (m.company_name || "").trim().charAt(0).toUpperCase();
if (letter === "#") {
@@ -63,20 +87,53 @@ export default function ManufacturerDirectoryClient({
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 className="flex gap-1 bg-[#0d0d0d] border border-[#252525] rounded-md p-1">
{SHOW_FILTERS.map((f) => (
</div>
{/* Per-show filter chips. Every event from bb.events shows up; ones
without exhibitor mappings yet render a "(soon)" badge so the
admin can see what's still missing data. */}
<div className="flex flex-wrap gap-1.5 mb-4">
<button
type="button"
onClick={() => setShowFilter("all")}
className={`px-3 py-1.5 rounded text-xs font-semibold ${
showFilter === "all" ? "bg-[#3b82f6] text-white" : "bg-[#0d0d0d] border border-[#252525] text-[#aaa] hover:bg-[#1a1a1a]"
}`}
>
All shows <span className="ml-1 opacity-60">({manufacturers.length.toLocaleString()})</span>
</button>
<button
type="button"
onClick={() => setShowFilter("both-major")}
className={`px-3 py-1.5 rounded text-xs font-semibold ${
showFilter === "both-major" ? "bg-[#3b82f6] text-white" : "bg-[#0d0d0d] border border-[#252525] text-[#aaa] hover:bg-[#1a1a1a]"
}`}
>
NAB + IBC <span className="ml-1 opacity-60">({manufacturers.filter((m) => m.exhibits_nab && m.exhibits_ibc).length})</span>
</button>
{shows.map((s) => {
const count = showCounts[s.slug] || 0;
const active = showFilter === s.slug;
const label = s.short_name || s.name;
return (
<button
key={f}
key={s.slug}
type="button"
onClick={() => setShowFilter(f)}
onClick={() => setShowFilter(s.slug)}
className={`px-3 py-1.5 rounded text-xs font-semibold ${
showFilter === f ? "bg-[#3b82f6] text-white" : "text-[#aaa] hover:bg-[#1a1a1a]"
active ? "bg-[#3b82f6] text-white" : "bg-[#0d0d0d] border border-[#252525] text-[#aaa] hover:bg-[#1a1a1a]"
}`}
title={`${s.name} · ${s.start_date}${count === 0 ? " · exhibitor list coming soon" : ""}`}
>
{f}
{label}
{count > 0 ? (
<span className="ml-1 opacity-60">({count})</span>
) : (
<span className="ml-1 text-amber-400 text-[10px]">soon</span>
)}
</button>
))}
</div>
);
})}
</div>
<nav className="flex flex-wrap gap-1 mb-6 text-xs">

View File

@@ -29,16 +29,26 @@ type ManufacturerRow = {
categories: string[] | null;
exhibits_nab: boolean;
exhibits_ibc: boolean;
shows_attending: string[] | null;
mention_count: number;
featured: boolean;
};
export type ShowOption = {
slug: string;
short_name: string | null;
name: string;
start_date: string;
end_date: string;
status: string;
};
export default async function ManufacturersIndex() {
const supabase = await createClient();
const { data, error } = await supabase
.from("tracked_companies")
.select(
"slug, company_name, bio, logo_url, categories, exhibits_nab, exhibits_ibc, mention_count, featured"
"slug, company_name, bio, logo_url, categories, exhibits_nab, exhibits_ibc, shows_attending, mention_count, featured"
)
.eq("directory_visible", true)
.not("slug", "is", null)
@@ -47,7 +57,17 @@ export default async function ManufacturersIndex() {
.order("company_name", { ascending: true })
.limit(2000);
// Every confirmed/tentative event from bb.events. Each event's slug
// (e.g. 'nab-show-2026') maps to tracked_companies.shows_attending so
// we can filter the directory by show.
const { data: eventsData } = await supabase
.from("events")
.select("slug, short_name, name, start_date, end_date, status")
.in("status", ["confirmed", "tentative"])
.order("start_date", { ascending: true });
const list = (data || []) as ManufacturerRow[];
const events = (eventsData || []) as ShowOption[];
const nabCount = list.filter((m) => m.exhibits_nab).length;
const ibcCount = list.filter((m) => m.exhibits_ibc).length;
@@ -72,7 +92,7 @@ export default async function ManufacturersIndex() {
)}
</header>
<ManufacturerDirectoryClient manufacturers={list} />
<ManufacturerDirectoryClient manufacturers={list} shows={events} />
<section className="mt-16 border-t border-[#252525] pt-8 text-sm text-[#888] leading-relaxed">
<p>