Manufacturers directory: fix light-theme text contrast and filter bar
This commit is contained in:
@@ -1,21 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
type Manufacturer = {
|
type Manufacturer = {
|
||||||
slug: string;
|
slug: string;
|
||||||
company_name: string;
|
company_name: string;
|
||||||
bio: string | null;
|
bio: string | null;
|
||||||
logo_url: string | null;
|
logo_url: string | null;
|
||||||
categories: string[] | null;
|
categories: string[] | null;
|
||||||
exhibits_nab: boolean;
|
exhibits_nab: boolean;
|
||||||
exhibits_ibc: boolean;
|
exhibits_ibc: boolean;
|
||||||
shows_attending: string[] | null;
|
shows_attending: string[] | null;
|
||||||
mention_count: number;
|
mention_count: number;
|
||||||
featured: boolean;
|
featured: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ShowOption = {
|
type ShowOption = {
|
||||||
slug: string;
|
slug: string;
|
||||||
short_name: string | null;
|
short_name: string | null;
|
||||||
@@ -25,184 +25,192 @@ type ShowOption = {
|
|||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ManufacturerDirectoryClient({
|
const LOGO_OVERRIDES: Record<string, string> = {
|
||||||
manufacturers,
|
"blackmagic-design": "/img/partners/blackmagic-design.png",
|
||||||
shows = [],
|
};
|
||||||
}: {
|
|
||||||
manufacturers: Manufacturer[];
|
|
||||||
shows?: ShowOption[];
|
|
||||||
}) {
|
|
||||||
const [q, setQ] = useState("");
|
|
||||||
// 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.
|
function resolveLogoUrl(slug: string, logoUrl: string | null): string | null {
|
||||||
const showCounts = useMemo(() => {
|
return LOGO_OVERRIDES[slug] || logoUrl || null;
|
||||||
const counts: Record<string, number> = {};
|
}
|
||||||
for (const m of manufacturers) {
|
|
||||||
for (const s of m.shows_attending || []) {
|
export default function ManufacturerDirectoryClient({
|
||||||
counts[s] = (counts[s] || 0) + 1;
|
manufacturers,
|
||||||
}
|
shows = [],
|
||||||
}
|
}: {
|
||||||
return counts;
|
manufacturers: Manufacturer[];
|
||||||
}, [manufacturers]);
|
shows?: ShowOption[];
|
||||||
|
}) {
|
||||||
const filtered = useMemo(() => {
|
const [q, setQ] = useState("");
|
||||||
const needle = q.trim().toLowerCase();
|
// showFilter is now a show slug from bb.events (e.g. 'nab-show-2026') or
|
||||||
return manufacturers.filter((m) => {
|
// 'all' / 'both-major' (NAB+IBC overlap).
|
||||||
if (needle) {
|
const [showFilter, setShowFilter] = useState<string>("all");
|
||||||
const hay = `${m.company_name} ${m.bio || ""}`.toLowerCase();
|
const [letter, setLetter] = useState<string>("All");
|
||||||
if (!hay.includes(needle)) return false;
|
|
||||||
}
|
// Per-show exhibitor counts for badge display + auto-hiding empty shows.
|
||||||
if (showFilter === "both-major") {
|
const showCounts = useMemo(() => {
|
||||||
if (!(m.exhibits_nab && m.exhibits_ibc)) return false;
|
const counts: Record<string, number> = {};
|
||||||
} else if (showFilter !== "all") {
|
for (const m of manufacturers) {
|
||||||
if (!(m.shows_attending || []).includes(showFilter)) return false;
|
for (const s of m.shows_attending || []) {
|
||||||
}
|
counts[s] = (counts[s] || 0) + 1;
|
||||||
if (letter !== "All") {
|
}
|
||||||
const c = (m.company_name || "").trim().charAt(0).toUpperCase();
|
}
|
||||||
if (letter === "#") {
|
return counts;
|
||||||
if (/^[A-Z]/.test(c)) return false;
|
}, [manufacturers]);
|
||||||
} else if (c !== letter) {
|
|
||||||
return false;
|
const filtered = useMemo(() => {
|
||||||
}
|
const needle = q.trim().toLowerCase();
|
||||||
}
|
return manufacturers.filter((m) => {
|
||||||
return true;
|
if (needle) {
|
||||||
});
|
const hay = `${m.company_name} ${m.bio || ""}`.toLowerCase();
|
||||||
}, [manufacturers, q, showFilter, letter]);
|
if (!hay.includes(needle)) return false;
|
||||||
|
}
|
||||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
if (showFilter === "both-major") {
|
||||||
|
if (!(m.exhibits_nab && m.exhibits_ibc)) return false;
|
||||||
return (
|
} else if (showFilter !== "all") {
|
||||||
<div>
|
if (!(m.shows_attending || []).includes(showFilter)) return false;
|
||||||
<div className="bg-[#111] border border-[#DCE6F2] rounded-xl p-4 mb-6 flex flex-col gap-3 md:flex-row md:items-center md:gap-4">
|
}
|
||||||
|
if (letter !== "All") {
|
||||||
|
const c = (m.company_name || "").trim().charAt(0).toUpperCase();
|
||||||
|
if (letter === "#") {
|
||||||
|
if (/^[A-Z]/.test(c)) return false;
|
||||||
|
} else if (c !== letter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [manufacturers, q, showFilter, letter]);
|
||||||
|
|
||||||
|
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="bg-white border border-[#DCE6F2] 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-[#F8FAFC] border border-[#DCE6F2] rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1D4ED8]/40"
|
className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-md px-3 py-2 text-sm text-[#0F172A] placeholder:text-[#94A3B8] focus:outline-none focus:ring-2 focus:ring-[#1D4ED8]/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Per-show filter chips. Every event from bb.events shows up; ones
|
{/* Per-show filter chips. Every event from bb.events shows up; ones
|
||||||
without exhibitor mappings yet render a "(soon)" badge so the
|
without exhibitor mappings yet render a "(soon)" badge so the
|
||||||
admin can see what's still missing data. */}
|
admin can see what's still missing data. */}
|
||||||
<div className="flex flex-wrap gap-1.5 mb-4">
|
<div className="flex flex-wrap gap-1.5 mb-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowFilter("all")}
|
onClick={() => setShowFilter("all")}
|
||||||
className={`px-3 py-1.5 rounded text-xs font-semibold ${
|
className={`px-3 py-1.5 rounded text-xs font-semibold ${
|
||||||
showFilter === "all" ? "bg-[#1D4ED8] text-white" : "bg-[#F8FAFC] border border-[#DCE6F2] text-[#aaa] hover:bg-[#FFFFFF]"
|
showFilter === "all" ? "bg-[#1D4ED8] text-white" : "bg-white border border-[#DCE6F2] text-[#475569] hover:bg-[#F8FAFC]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
All shows <span className="ml-1 opacity-60">({manufacturers.length.toLocaleString()})</span>
|
All shows <span className="ml-1 opacity-60">({manufacturers.length.toLocaleString()})</span>
|
||||||
</button>
|
</button>
|
||||||
{shows.map((s) => {
|
{shows.map((s) => {
|
||||||
const count = showCounts[s.slug] || 0;
|
const count = showCounts[s.slug] || 0;
|
||||||
const active = showFilter === s.slug;
|
const active = showFilter === s.slug;
|
||||||
const label = s.short_name || s.name;
|
const label = s.short_name || s.name;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={s.slug}
|
key={s.slug}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowFilter(s.slug)}
|
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 ${
|
||||||
active ? "bg-[#1D4ED8] text-white" : "bg-[#F8FAFC] border border-[#DCE6F2] text-[#aaa] hover:bg-[#FFFFFF]"
|
active ? "bg-[#1D4ED8] text-white" : "bg-white border border-[#DCE6F2] text-[#475569] hover:bg-[#F8FAFC]"
|
||||||
}`}
|
}`}
|
||||||
title={`${s.name} · ${s.start_date}${count === 0 ? " · exhibitor list coming soon" : ""}`}
|
title={`${s.name} · ${s.start_date}${count === 0 ? " · exhibitor list coming soon" : ""}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{count > 0 ? (
|
{count > 0 ? (
|
||||||
<span className="ml-1 opacity-60">({count})</span>
|
<span className="ml-1 opacity-60">({count})</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="ml-1 text-amber-400 text-[10px]">soon</span>
|
<span className="ml-1 text-amber-700 text-[10px]">soon</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex flex-wrap gap-1 mb-6 text-xs">
|
<nav className="flex flex-wrap gap-1 mb-6 text-xs">
|
||||||
{(["All", "#", ...letters] as const).map((l) => (
|
{(["All", "#", ...letters] as const).map((l) => (
|
||||||
<button
|
<button
|
||||||
key={l}
|
key={l}
|
||||||
type="button"
|
type="button"
|
||||||
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-[#1D4ED8] text-white"
|
? "bg-[#1D4ED8] text-white"
|
||||||
: "bg-[#F8FAFC] border border-[#DCE6F2] text-[#aaa] hover:bg-[#FFFFFF]"
|
: "bg-white border border-[#DCE6F2] text-[#475569] hover:bg-[#F8FAFC]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{l}
|
{l}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<p className="text-xs text-[#888] 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>
|
||||||
|
|
||||||
<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">
|
||||||
{filtered.map((m) => (
|
{filtered.map((m) => (
|
||||||
<Link
|
<Link
|
||||||
key={m.slug}
|
key={m.slug}
|
||||||
href={`/manufacturers/${m.slug}`}
|
href={`/manufacturers/${m.slug}`}
|
||||||
className="group bg-[#F8FAFC] border border-[#DCE6F2] rounded-xl p-4 hover:border-[#1D4ED8] hover:shadow-sm transition-all flex gap-3"
|
className="group bg-white border border-[#DCE6F2] rounded-xl p-4 hover:border-[#1D4ED8] hover:shadow-sm transition-all flex gap-3"
|
||||||
>
|
>
|
||||||
<div className="w-14 h-14 rounded-lg bg-[#FFFFFF] flex items-center justify-center overflow-hidden flex-shrink-0">
|
<div className="w-14 h-14 rounded-lg bg-[#FFFFFF] flex items-center justify-center overflow-hidden flex-shrink-0">
|
||||||
{m.logo_url ? (
|
{resolveLogoUrl(m.slug, m.logo_url) ? (
|
||||||
/* eslint-disable-next-line @next/next/no-img-element */
|
/* eslint-disable-next-line @next/next/no-img-element */
|
||||||
<img
|
<img
|
||||||
src={m.logo_url}
|
src={resolveLogoUrl(m.slug, m.logo_url)!}
|
||||||
alt={`${m.company_name} logo`}
|
alt={`${m.company_name} logo`}
|
||||||
className="w-full h-full object-contain"
|
className="w-full h-full object-contain p-1"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-[#666] 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-[#e0e0e0] truncate group-hover:text-[#1D4ED8] transition-colors">
|
<h3 className="font-semibold text-[#0F172A] truncate group-hover:text-[#1D4ED8] 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">
|
||||||
{(m.shows_attending || []).slice(0, 3).map((slug) => {
|
{(m.shows_attending || []).slice(0, 3).map((slug) => {
|
||||||
const ev = shows.find((s) => s.slug === slug);
|
const ev = shows.find((s) => s.slug === slug);
|
||||||
if (!ev) return null;
|
if (!ev) return null;
|
||||||
return (
|
return (
|
||||||
<span key={slug} className="text-[10px] font-semibold uppercase tracking-wider bg-[#1D4ED8]/10 text-[#1D4ED8] px-1.5 py-0.5 rounded">
|
<span key={slug} className="text-[10px] font-semibold uppercase tracking-wider bg-[#1D4ED8]/10 text-[#1D4ED8] px-1.5 py-0.5 rounded">
|
||||||
{ev.short_name || ev.name}
|
{ev.short_name || ev.name}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{m.featured && (
|
{m.featured && (
|
||||||
<span className="text-[10px] font-semibold uppercase tracking-wider bg-amber-50 text-amber-700 px-1.5 py-0.5 rounded">
|
<span className="text-[10px] font-semibold uppercase tracking-wider bg-amber-50 text-amber-700 px-1.5 py-0.5 rounded">
|
||||||
Featured
|
Featured
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-[#888] line-clamp-2">
|
<p className="text-xs text-[#64748B] line-clamp-2">
|
||||||
{m.bio || "Profile coming soon."}
|
{m.bio || "Profile coming soon."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<div className="text-center py-12 text-[#888] text-sm">
|
<div className="text-center py-12 text-[#888] text-sm">
|
||||||
No manufacturers match these filters.
|
No manufacturers match these filters.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user