273 lines
11 KiB
TypeScript
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 · 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 & 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>
|
|
);
|
|
} |