Files
avbeat-com/src/app/about/team/page.tsx

273 lines
11 KiB
TypeScript

import { createClient } from "@supabase/supabase-js";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import SidebarAdStack from "@/components/SidebarAdStack";
import ObfuscatedEmail from "@/components/ObfuscatedEmail";
export const dynamic = "force-dynamic";
export const revalidate = 600;
export const metadata = {
title: "Our team — AV Beat",
description: "Meet the AV Beat team — editorial staff plus accounts and support, covering pro AV, live production, and display tech.",
};
interface Persona {
slug: string;
name: string;
bio: string | null;
beat: string | null;
avatar_url: string | null;
}
interface OpsPersona {
id: string;
display_name: string;
role: string;
email: string | null;
phone: string | null;
extension: string | null;
avatar_url: string | null;
}
export default async function TeamPage() {
const sb = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
db: { schema: (process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb") as "public" },
auth: { persistSession: false },
}
);
const { data } = await sb
.from("ai_personas")
.select("slug, name, bio, beat, avatar_url")
.eq("active", true);
const raw = (data || []) as Persona[];
const team = [
...raw.filter((p) => p.slug === "ryan-salazar"),
...raw
.filter((p) => p.slug !== "ryan-salazar")
.sort((a, b) => a.name.localeCompare(b.name)),
];
const sbOps = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ db: { schema: "pulsedesk" as "public" }, auth: { persistSession: false } }
);
const { data: opsData } = await sbOps
.from("personas")
.select("id, display_name, role, email, phone, extension, avatar_url")
.eq("active", true)
.in("id", ["chloe", "jeff", "riley", "brian", "avery"]);
const ops = (opsData || []) as OpsPersona[];
const displayBeat = (p: Persona) =>
p.slug === "ryan-salazar" ? "Founder / Editor-in-Chief" : p.beat?.replace(/-/g, " ") ?? "";
const formatPhone = (raw: string): string => {
const digits = raw.replace(/[^0-9]/g, "");
const local = digits.startsWith("1") && digits.length === 11 ? digits.slice(1) : digits;
if (local.length !== 10) return raw;
return `(${local.slice(0, 3)}) ${local.slice(3, 6)}-${local.slice(6)}`;
};
const editorialEmail = (p: Persona): { user: string; domain: string } | null => {
if (!p.slug) return null;
return { user: p.slug.replace(/-/g, "."), domain: "avbeat.com" };
};
const splitEmail = (email: string): { user: string; domain: string } | null => {
const at = email.indexOf("@");
if (at <= 0) return null;
return { user: email.slice(0, at), domain: email.slice(at + 1) };
};
const tableWrap = "overflow-x-auto rounded-xl border border-[#1a1f2a] bg-[#0b0f17]";
const tableClass = "w-full min-w-[640px] text-sm text-left border-collapse";
const thClass =
"px-4 py-3 text-[10px] font-mono uppercase tracking-[0.14em] text-[#93C5FD] bg-[#141a25] border-b border-[#1a1f2a] whitespace-nowrap";
const tdClass = "px-4 py-4 align-top border-b border-[#1a1f2a] text-[#aab1bd]";
const trHover = "hover:bg-[#111827]/60 transition-colors";
return (
<div className="min-h-screen bg-background">
<Header />
<div className="max-w-container mx-auto px-4 py-12 text-[#0F172A]">
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
<main>
<header className="brand-gradient rounded-xl mb-10">
<div className="px-6 py-10 sm:px-10 sm:py-14">
<div className="inline-flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.22em] text-white/85 mb-3">
<span className="w-6 h-px bg-white/60" />
AV Beat &middot; editorial
</div>
<h1 className="font-display text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight text-white">
The team behind every byline
</h1>
<p className="mt-4 text-white/80 text-base md:text-lg max-w-2xl leading-relaxed">
The writers, editors, and operators behind the AV Beat newsroom
covering pro AV, live production, and display tech every day.
</p>
</div>
</header>
<section className="mb-14">
<div className="flex items-baseline justify-between mb-5">
<h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight">Editorial</h2>
<span className="text-[10px] font-mono uppercase tracking-[0.18em] text-[#6c7686]">
{team.length} {team.length === 1 ? "writer" : "writers"}
</span>
</div>
<div className={tableWrap}>
<table className={tableClass}>
<thead>
<tr>
<th className={thClass}>Name</th>
<th className={thClass}>Role</th>
<th className={thClass}>Focus</th>
<th className={thClass}>Contact</th>
</tr>
</thead>
<tbody>
{team.map((p) => {
const e = editorialEmail(p);
return (
<tr key={p.slug} className={trHover}>
<td className={tdClass}>
<a
href={`/authors/${p.slug}`}
className="font-display text-base font-bold text-white hover:text-[#7DD3FC] hover:underline"
>
{p.name}
</a>
</td>
<td className={tdClass}>
{(p.beat || p.slug === "ryan-salazar") && (
<span className="inline-block px-2 py-0.5 rounded-sm bg-[#1D4ED8]/15 border border-[#1D4ED8]/35 text-[10px] font-mono uppercase tracking-[0.12em] text-[#93C5FD]">
{displayBeat(p)}
</span>
)}
</td>
<td className={`${tdClass} max-w-md`}>
{p.bio ? (
<p className="leading-relaxed">{p.bio}</p>
) : (
<span className="text-[#6c7686]"></span>
)}
</td>
<td className={tdClass}>
<div className="space-y-2">
{e && (
<ObfuscatedEmail
user={e.user}
domain={e.domain}
className="text-[#aab1bd] hover:text-[#7DD3FC] font-mono block"
/>
)}
<a
href={`/authors/${p.slug}`}
className="inline-block text-[10px] font-mono uppercase tracking-[0.12em] text-[#6c7686] hover:text-[#7DD3FC]"
>
Profile
</a>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</section>
{ops.length > 0 && (
<section>
<div className="flex items-baseline justify-between mb-5">
<h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight">
Accounts &amp; support
</h2>
<span className="text-[10px] font-mono uppercase tracking-[0.18em] text-[#6c7686]">
{ops.length} {ops.length === 1 ? "contact" : "contacts"}
</span>
</div>
<p className="text-[#475569] text-sm mb-5 max-w-2xl">
For billing, accounts receivable, vendor onboarding, sales, and
general support questions these are the people to reach.
</p>
<div className={tableWrap}>
<table className={tableClass}>
<thead>
<tr>
<th className={thClass}>Name</th>
<th className={thClass}>Role</th>
<th className={thClass}>Email</th>
<th className={thClass}>Phone</th>
</tr>
</thead>
<tbody>
{ops.map((p) => (
<tr key={p.id} className={trHover}>
<td className={tdClass}>
<span className="font-display text-base font-bold text-white">
{p.display_name}
</span>
</td>
<td className={tdClass}>
<span className="inline-block px-2 py-0.5 rounded-sm bg-[#1D4ED8]/15 border border-[#1D4ED8]/35 text-[10px] font-mono uppercase tracking-[0.12em] text-[#93C5FD]">
{p.role}
</span>
</td>
<td className={tdClass}>
{p.email ? (
(() => {
const e = splitEmail(p.email!);
if (!e) return null;
return (
<ObfuscatedEmail
user={e.user}
domain={e.domain}
className="text-[#aab1bd] hover:text-[#7DD3FC] font-mono block"
/>
);
})()
) : (
<span className="text-[#6c7686]"></span>
)}
</td>
<td className={tdClass}>
{p.phone ? (
<a
href={`tel:${p.phone.replace(/[^\d+]/g, "")}${p.extension ? "," + p.extension : ""}`}
className="hover:text-[#7DD3FC] font-mono text-[#aab1bd]"
>
{formatPhone(p.phone)}
{p.extension ? `, Ext. ${p.extension}` : ""}
</a>
) : (
<span className="text-[#6c7686]"></span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
</main>
<aside className="mt-10 lg:mt-0" aria-label="Advertisements">
<div className="lg:sticky lg:top-24">
<SidebarAdStack />
</div>
</aside>
</div>
</div>
<Footer />
</div>
);
}