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;
|
categories: string[] | null;
|
||||||
exhibits_nab: boolean;
|
exhibits_nab: boolean;
|
||||||
exhibits_ibc: boolean;
|
exhibits_ibc: boolean;
|
||||||
|
shows_attending: string[] | null;
|
||||||
mention_count: number;
|
mention_count: number;
|
||||||
featured: boolean;
|
featured: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SHOW_FILTERS = ["All", "NAB Show", "IBC", "Both shows"] as const;
|
type ShowOption = {
|
||||||
type ShowFilter = (typeof SHOW_FILTERS)[number];
|
slug: string;
|
||||||
|
short_name: string | null;
|
||||||
|
name: string;
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function ManufacturerDirectoryClient({
|
export default function ManufacturerDirectoryClient({
|
||||||
manufacturers,
|
manufacturers,
|
||||||
|
shows = [],
|
||||||
}: {
|
}: {
|
||||||
manufacturers: Manufacturer[];
|
manufacturers: Manufacturer[];
|
||||||
|
shows?: ShowOption[];
|
||||||
}) {
|
}) {
|
||||||
const [q, setQ] = useState("");
|
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");
|
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 filtered = useMemo(() => {
|
||||||
const needle = q.trim().toLowerCase();
|
const needle = q.trim().toLowerCase();
|
||||||
return manufacturers.filter((m) => {
|
return manufacturers.filter((m) => {
|
||||||
@@ -34,9 +56,11 @@ export default function ManufacturerDirectoryClient({
|
|||||||
const hay = `${m.company_name} ${m.bio || ""}`.toLowerCase();
|
const hay = `${m.company_name} ${m.bio || ""}`.toLowerCase();
|
||||||
if (!hay.includes(needle)) return false;
|
if (!hay.includes(needle)) return false;
|
||||||
}
|
}
|
||||||
if (showFilter === "NAB Show" && !m.exhibits_nab) return false;
|
if (showFilter === "both-major") {
|
||||||
if (showFilter === "IBC" && !m.exhibits_ibc) return false;
|
if (!(m.exhibits_nab && m.exhibits_ibc)) return false;
|
||||||
if (showFilter === "Both shows" && !(m.exhibits_nab && m.exhibits_ibc)) return false;
|
} else if (showFilter !== "all") {
|
||||||
|
if (!(m.shows_attending || []).includes(showFilter)) return false;
|
||||||
|
}
|
||||||
if (letter !== "All") {
|
if (letter !== "All") {
|
||||||
const c = (m.company_name || "").trim().charAt(0).toUpperCase();
|
const c = (m.company_name || "").trim().charAt(0).toUpperCase();
|
||||||
if (letter === "#") {
|
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"
|
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-[#0d0d0d] border border-[#252525] rounded-md p-1">
|
</div>
|
||||||
{SHOW_FILTERS.map((f) => (
|
|
||||||
|
{/* 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
|
<button
|
||||||
key={f}
|
key={s.slug}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowFilter(f)}
|
onClick={() => setShowFilter(s.slug)}
|
||||||
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-[#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>
|
</button>
|
||||||
))}
|
);
|
||||||
</div>
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex flex-wrap gap-1 mb-6 text-xs">
|
<nav className="flex flex-wrap gap-1 mb-6 text-xs">
|
||||||
|
|||||||
@@ -29,16 +29,26 @@ type ManufacturerRow = {
|
|||||||
categories: string[] | null;
|
categories: string[] | null;
|
||||||
exhibits_nab: boolean;
|
exhibits_nab: boolean;
|
||||||
exhibits_ibc: boolean;
|
exhibits_ibc: boolean;
|
||||||
|
shows_attending: string[] | null;
|
||||||
mention_count: number;
|
mention_count: number;
|
||||||
featured: boolean;
|
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() {
|
export default async function ManufacturersIndex() {
|
||||||
const supabase = await createClient();
|
const supabase = await createClient();
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("tracked_companies")
|
.from("tracked_companies")
|
||||||
.select(
|
.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)
|
.eq("directory_visible", true)
|
||||||
.not("slug", "is", null)
|
.not("slug", "is", null)
|
||||||
@@ -47,7 +57,17 @@ export default async function ManufacturersIndex() {
|
|||||||
.order("company_name", { ascending: true })
|
.order("company_name", { ascending: true })
|
||||||
.limit(2000);
|
.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 list = (data || []) as ManufacturerRow[];
|
||||||
|
const events = (eventsData || []) as ShowOption[];
|
||||||
const nabCount = list.filter((m) => m.exhibits_nab).length;
|
const nabCount = list.filter((m) => m.exhibits_nab).length;
|
||||||
const ibcCount = list.filter((m) => m.exhibits_ibc).length;
|
const ibcCount = list.filter((m) => m.exhibits_ibc).length;
|
||||||
|
|
||||||
@@ -72,7 +92,7 @@ export default async function ManufacturersIndex() {
|
|||||||
)}
|
)}
|
||||||
</header>
|
</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">
|
<section className="mt-16 border-t border-[#252525] pt-8 text-sm text-[#888] leading-relaxed">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
Reference in New Issue
Block a user