manufacturers/[slug]: executive leadership + phone + street address

Renders bb.manufacturer_executives rows as a card grid (photo, name,
title, bio excerpt, LinkedIn link). Adds a Headquarters section with
the street-level hq_address + phone (tel: link) + contact email. Bio
text is now whitespace-preserving so multi-paragraph copy from the
admin editor renders correctly.
This commit is contained in:
Ryan Salazar
2026-05-26 23:45:04 +00:00
parent 8c4721bcbd
commit d82b8edd21

View File

@@ -14,21 +14,19 @@ async function loadManufacturer(slug: string) {
const { data } = await supabase const { data } = await supabase
.from("tracked_companies") .from("tracked_companies")
.select( .select(
"id, slug, company_name, bio, bio_source, logo_url, company_website, contact_email, contact_url, hq_city, hq_country, linkedin_url, twitter_handle, youtube_url, press_url, categories, exhibits_nab, exhibits_ibc, mention_count, last_mentioned, featured, ad_status" "id, slug, company_name, bio, bio_source, logo_url, company_website, contact_email, contact_url, phone, hq_address, hq_city, hq_country, linkedin_url, twitter_handle, youtube_url, press_url, categories, exhibits_nab, exhibits_ibc, mention_count, last_mentioned, featured, ad_status"
) )
.eq("slug", slug) .eq("slug", slug)
.eq("directory_visible", true) .eq("directory_visible", true)
.maybeSingle(); .maybeSingle();
if (!data) return null; if (!data) return null;
// Per-show appearances
const { data: shows } = await supabase const { data: shows } = await supabase
.from("trade_show_exhibitors") .from("trade_show_exhibitors")
.select("show, show_year, booth, description, url, website") .select("show, show_year, booth, description, url, website")
.eq("exhibitor_name", data.company_name) .eq("exhibitor_name", data.company_name)
.order("show_year", { ascending: false }); .order("show_year", { ascending: false });
// Recent BB stories that mention this company (mention_count guides interest)
const { data: pressReleases } = await supabase const { data: pressReleases } = await supabase
.from("press_releases") .from("press_releases")
.select("title, slug, published_at, source_url") .select("title, slug, published_at, source_url")
@@ -36,7 +34,19 @@ async function loadManufacturer(slug: string) {
.order("published_at", { ascending: false }) .order("published_at", { ascending: false })
.limit(10); .limit(10);
return { company: data, shows: shows || [], pressReleases: pressReleases || [] }; const { data: executives } = await supabase
.from("manufacturer_executives")
.select("id, name, title, photo_url, linkedin_url, bio")
.eq("company_id", data.id)
.order("sort_order")
.order("name");
return {
company: data,
shows: shows || [],
pressReleases: pressReleases || [],
executives: executives || [],
};
} }
export async function generateMetadata({ export async function generateMetadata({
@@ -76,7 +86,7 @@ export default async function ManufacturerProfile({
const { slug } = await params; const { slug } = await params;
const result = await loadManufacturer(slug); const result = await loadManufacturer(slug);
if (!result) notFound(); if (!result) notFound();
const { company, shows, pressReleases } = result; const { company, shows, pressReleases, executives } = result;
const jsonLd = { const jsonLd = {
"@context": "https://schema.org", "@context": "https://schema.org",
@@ -149,6 +159,11 @@ export default async function ManufacturerProfile({
{[company.hq_city, company.hq_country].filter(Boolean).join(", ")} {[company.hq_city, company.hq_country].filter(Boolean).join(", ")}
</span> </span>
)} )}
{company.phone && (
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-ink/60 hover:text-brand-primary">
{company.phone}
</a>
)}
</div> </div>
<div className="mt-3 flex flex-wrap gap-3 text-sm"> <div className="mt-3 flex flex-wrap gap-3 text-sm">
{company.company_website && ( {company.company_website && (
@@ -208,7 +223,7 @@ 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-ink mb-2">About</h2>
<p className="text-ink/80 leading-relaxed">{company.bio}</p> <div className="text-ink/80 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-ink/40 mt-2">
Source: {company.bio_source} Source: {company.bio_source}
@@ -217,6 +232,72 @@ export default async function ManufacturerProfile({
</section> </section>
)} )}
{executives.length > 0 && (
<section className="mb-10">
<h2 className="text-lg font-display font-bold text-ink mb-4">Executive leadership</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{executives.map((e: any) => (
<div key={e.id} className="bg-surface-soft border border-ink/10 rounded-lg p-4 flex gap-3">
{e.photo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */
<img
src={e.photo_url}
alt={e.name}
className="w-16 h-16 rounded-full object-cover bg-ink/5 flex-shrink-0"
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">
{(e.name || "?").trim().charAt(0).toUpperCase()}
</div>
)}
<div className="min-w-0">
<div className="font-semibold text-ink truncate">{e.name}</div>
{e.title && <div className="text-xs text-ink/60 mb-1">{e.title}</div>}
{e.bio && <p className="text-xs text-ink/70 line-clamp-3 mb-1">{e.bio}</p>}
{e.linkedin_url && (
<a
href={e.linkedin_url}
target="_blank"
rel="noopener noreferrer"
className="text-[11px] text-brand-primary hover:underline"
>
LinkedIn
</a>
)}
</div>
</div>
))}
</div>
</section>
)}
{(company.hq_address || company.phone || company.contact_email) && (
<section className="mb-10">
<h2 className="text-lg font-display font-bold text-ink mb-2">Headquarters</h2>
<address className="not-italic text-ink/80 text-sm leading-relaxed">
{company.hq_address && <div>{company.hq_address}</div>}
{(company.hq_city || company.hq_country) && (
<div>{[company.hq_city, company.hq_country].filter(Boolean).join(", ")}</div>
)}
{company.phone && (
<div className="mt-1">
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-brand-primary hover:underline">
{company.phone}
</a>
</div>
)}
{company.contact_email && (
<div>
<a href={`mailto:${company.contact_email}`} className="text-brand-primary hover:underline">
{company.contact_email}
</a>
</div>
)}
</address>
</section>
)}
{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-ink mb-2">Categories</h2>