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:
@@ -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
|
||||
key={f}
|
||||
type="button"
|
||||
onClick={() => setShowFilter(f)}
|
||||
onClick={() => setShowFilter("all")}
|
||||
className={`px-3 py-1.5 rounded text-xs font-semibold ${
|
||||
showFilter === f ? "bg-[#3b82f6] text-white" : "text-[#aaa] hover:bg-[#1a1a1a]"
|
||||
showFilter === "all" ? "bg-[#3b82f6] text-white" : "bg-[#0d0d0d] border border-[#252525] text-[#aaa] hover:bg-[#1a1a1a]"
|
||||
}`}
|
||||
>
|
||||
{f}
|
||||
All shows <span className="ml-1 opacity-60">({manufacturers.length.toLocaleString()})</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<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={s.slug}
|
||||
type="button"
|
||||
onClick={() => setShowFilter(s.slug)}
|
||||
className={`px-3 py-1.5 rounded text-xs font-semibold ${
|
||||
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" : ""}`}
|
||||
>
|
||||
{label}
|
||||
{count > 0 ? (
|
||||
<span className="ml-1 opacity-60">({count})</span>
|
||||
) : (
|
||||
<span className="ml-1 text-amber-400 text-[10px]">soon</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-wrap gap-1 mb-6 text-xs">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user