Team page: table layout without photos
This commit is contained in:
@@ -44,7 +44,6 @@ export default async function TeamPage() {
|
|||||||
.select("slug, name, bio, beat, avatar_url")
|
.select("slug, name, bio, beat, avatar_url")
|
||||||
.eq("active", true);
|
.eq("active", true);
|
||||||
const raw = (data || []) as Persona[];
|
const raw = (data || []) as Persona[];
|
||||||
// Ryan Salazar pinned first as Founder / Editor-in-Chief; rest alphabetical.
|
|
||||||
const team = [
|
const team = [
|
||||||
...raw.filter((p) => p.slug === "ryan-salazar"),
|
...raw.filter((p) => p.slug === "ryan-salazar"),
|
||||||
...raw
|
...raw
|
||||||
@@ -52,8 +51,6 @@ export default async function TeamPage() {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Operations staff — pulsedesk personas (accounts receivable, support).
|
|
||||||
// Only the company-wide RMP roles, not property-specific auto-attendants.
|
|
||||||
const sbOps = createClient(
|
const sbOps = createClient(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
@@ -67,18 +64,8 @@ export default async function TeamPage() {
|
|||||||
const ops = (opsData || []) as OpsPersona[];
|
const ops = (opsData || []) as OpsPersona[];
|
||||||
|
|
||||||
const displayBeat = (p: Persona) =>
|
const displayBeat = (p: Persona) =>
|
||||||
p.slug === "ryan-salazar" ? "FOUNDER / EDITOR-IN-CHIEF" : p.beat?.replace(/-/g, " ");
|
p.slug === "ryan-salazar" ? "Founder / Editor-in-Chief" : p.beat?.replace(/-/g, " ") ?? "";
|
||||||
|
|
||||||
const initials = (name: string) =>
|
|
||||||
name
|
|
||||||
.split(" ")
|
|
||||||
.filter(Boolean)
|
|
||||||
.slice(0, 2)
|
|
||||||
.map((w) => w.charAt(0).toUpperCase())
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
// Format an E.164 US number as (XXX) XXX-XXXX. Falls back to the raw
|
|
||||||
// string for international or non-standard inputs.
|
|
||||||
const formatPhone = (raw: string): string => {
|
const formatPhone = (raw: string): string => {
|
||||||
const digits = raw.replace(/[^0-9]/g, "");
|
const digits = raw.replace(/[^0-9]/g, "");
|
||||||
const local = digits.startsWith("1") && digits.length === 11 ? digits.slice(1) : digits;
|
const local = digits.startsWith("1") && digits.length === 11 ? digits.slice(1) : digits;
|
||||||
@@ -86,8 +73,6 @@ export default async function TeamPage() {
|
|||||||
return `(${local.slice(0, 3)}) ${local.slice(3, 6)}-${local.slice(6)}`;
|
return `(${local.slice(0, 3)}) ${local.slice(3, 6)}-${local.slice(6)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Derive @avbeat.com email from the persona slug. Real mailboxes
|
|
||||||
// are set up centrally; this is the authoring address surfaced to readers.
|
|
||||||
const editorialEmail = (p: Persona): { user: string; domain: string } | null => {
|
const editorialEmail = (p: Persona): { user: string; domain: string } | null => {
|
||||||
if (!p.slug) return null;
|
if (!p.slug) return null;
|
||||||
return { user: p.slug.replace(/-/g, "."), domain: "avbeat.com" };
|
return { user: p.slug.replace(/-/g, "."), domain: "avbeat.com" };
|
||||||
@@ -99,13 +84,19 @@ export default async function TeamPage() {
|
|||||||
return { user: email.slice(0, at), domain: email.slice(at + 1) };
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<Header />
|
<Header />
|
||||||
<div className="max-w-container mx-auto px-4 py-12 text-[#0F172A]">
|
<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">
|
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
|
||||||
<main>
|
<main>
|
||||||
{/* Hero — brand-gradient (animated sheen) */}
|
|
||||||
<header className="brand-gradient rounded-xl mb-10">
|
<header className="brand-gradient rounded-xl mb-10">
|
||||||
<div className="px-6 py-10 sm:px-10 sm:py-14">
|
<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">
|
<div className="inline-flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.22em] text-white/85 mb-3">
|
||||||
@@ -122,7 +113,6 @@ export default async function TeamPage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Editorial team — three-col grid on desktop, larger portraits */}
|
|
||||||
<section className="mb-14">
|
<section className="mb-14">
|
||||||
<div className="flex items-baseline justify-between mb-5">
|
<div className="flex items-baseline justify-between mb-5">
|
||||||
<h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight">Editorial</h2>
|
<h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight">Editorial</h2>
|
||||||
@@ -131,75 +121,68 @@ export default async function TeamPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
<div className={tableWrap}>
|
||||||
{team.map((p) => {
|
<table className={tableClass}>
|
||||||
const e = editorialEmail(p);
|
<thead>
|
||||||
return (
|
<tr>
|
||||||
<li
|
<th className={thClass}>Name</th>
|
||||||
key={p.slug}
|
<th className={thClass}>Role</th>
|
||||||
className="group relative rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 hover:border-[#1D4ED8]/50 hover:shadow-[0_0_0_1px_rgba(29,78,216,0.20)] transition-all"
|
<th className={thClass}>Focus</th>
|
||||||
>
|
<th className={thClass}>Contact</th>
|
||||||
{/* Top accent */}
|
</tr>
|
||||||
<div className="absolute top-0 left-5 right-5 h-px bg-gradient-to-r from-transparent via-[#1D4ED8]/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" aria-hidden />
|
</thead>
|
||||||
|
<tbody>
|
||||||
<div className="flex items-start gap-4">
|
{team.map((p) => {
|
||||||
{p.avatar_url ? (
|
const e = editorialEmail(p);
|
||||||
<a href={`/authors/${p.slug}`} className="flex-shrink-0">
|
return (
|
||||||
<img
|
<tr key={p.slug} className={trHover}>
|
||||||
src={p.avatar_url}
|
<td className={tdClass}>
|
||||||
alt={p.name}
|
<a
|
||||||
width={96}
|
href={`/authors/${p.slug}`}
|
||||||
height={96}
|
className="font-display text-base font-bold text-white hover:text-[#7DD3FC] hover:underline"
|
||||||
className="w-24 h-24 rounded-full border-2 border-[#1a1f2a] group-hover:border-[#1D4ED8]/40 transition-colors object-cover"
|
>
|
||||||
/>
|
{p.name}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
</td>
|
||||||
<div className="w-24 h-24 rounded-full border-2 border-[#1a1f2a] bg-[#141a25] flex items-center justify-center font-display font-bold text-2xl text-[#6c7686] flex-shrink-0">
|
<td className={tdClass}>
|
||||||
{initials(p.name)}
|
{(p.beat || p.slug === "ryan-salazar") && (
|
||||||
</div>
|
<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)}
|
||||||
<div className="flex-1 min-w-0">
|
</span>
|
||||||
<a href={`/authors/${p.slug}`} className="block hover:underline">
|
)}
|
||||||
<h3 className="font-display text-xl font-bold leading-tight text-white">{p.name}</h3>
|
</td>
|
||||||
</a>
|
<td className={`${tdClass} max-w-md`}>
|
||||||
{(p.beat || p.slug === "ryan-salazar") && (
|
{p.bio ? (
|
||||||
<div className="inline-block mt-1.5 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 className="leading-relaxed">{p.bio}</p>
|
||||||
{displayBeat(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>
|
</div>
|
||||||
)}
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
);
|
||||||
|
})}
|
||||||
{p.bio && (
|
</tbody>
|
||||||
<p className="text-sm text-[#aab1bd] mt-4 leading-relaxed line-clamp-4">
|
</table>
|
||||||
{p.bio}
|
</div>
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{e && (
|
|
||||||
<div className="mt-4 pt-4 border-t border-[#1a1f2a] flex items-center justify-between gap-3 text-xs">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<ObfuscatedEmail
|
|
||||||
user={e.user}
|
|
||||||
domain={e.domain}
|
|
||||||
className="text-[#aab1bd] hover:text-[#7DD3FC] font-mono block truncate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={`/authors/${p.slug}`}
|
|
||||||
className="flex-shrink-0 text-[10px] font-mono uppercase tracking-[0.12em] text-[#6c7686] hover:text-[#7DD3FC]"
|
|
||||||
>
|
|
||||||
View →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Operations — denser layout */}
|
|
||||||
{ops.length > 0 && (
|
{ops.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-baseline justify-between mb-5">
|
<div className="flex items-baseline justify-between mb-5">
|
||||||
@@ -215,52 +198,64 @@ export default async function TeamPage() {
|
|||||||
general support questions — these are the people to reach.
|
general support questions — these are the people to reach.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
<div className={tableWrap}>
|
||||||
{ops.map((p) => (
|
<table className={tableClass}>
|
||||||
<li
|
<thead>
|
||||||
key={p.id}
|
<tr>
|
||||||
className="rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 flex gap-4 hover:border-[#1D4ED8]/40 transition-colors"
|
<th className={thClass}>Name</th>
|
||||||
>
|
<th className={thClass}>Role</th>
|
||||||
{p.avatar_url ? (
|
<th className={thClass}>Email</th>
|
||||||
<img
|
<th className={thClass}>Phone</th>
|
||||||
src={p.avatar_url}
|
</tr>
|
||||||
alt={p.display_name}
|
</thead>
|
||||||
width={80}
|
<tbody>
|
||||||
height={80}
|
{ops.map((p) => (
|
||||||
className="w-20 h-20 rounded-full border-2 border-[#1a1f2a] flex-shrink-0 object-cover"
|
<tr key={p.id} className={trHover}>
|
||||||
/>
|
<td className={tdClass}>
|
||||||
) : (
|
<span className="font-display text-base font-bold text-white">
|
||||||
<div className="w-20 h-20 rounded-full border-2 border-[#1a1f2a] flex-shrink-0 bg-[#141a25] flex items-center justify-center font-display font-bold text-xl text-[#6c7686]">
|
{p.display_name}
|
||||||
{initials(p.display_name)}
|
</span>
|
||||||
</div>
|
</td>
|
||||||
)}
|
<td className={tdClass}>
|
||||||
<div className="flex-1 min-w-0">
|
<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]">
|
||||||
<h3 className="font-display text-lg font-bold leading-tight text-white">{p.display_name}</h3>
|
{p.role}
|
||||||
<div className="inline-block mt-1 px-2 py-0.5 rounded-sm bg-[#1D4ED8]/15 border border-[#1D4ED8]/35 text-[10px] font-mono uppercase tracking-[0.12em] text-[#93C5FD]">
|
</span>
|
||||||
{p.role}
|
</td>
|
||||||
</div>
|
<td className={tdClass}>
|
||||||
<div className="text-xs text-[#aab1bd] mt-3 space-y-1">
|
{p.email ? (
|
||||||
{p.email && (() => {
|
(() => {
|
||||||
const e = splitEmail(p.email);
|
const e = splitEmail(p.email!);
|
||||||
if (!e) return null;
|
if (!e) return null;
|
||||||
return (
|
return (
|
||||||
<div className="font-mono min-w-0">
|
<ObfuscatedEmail
|
||||||
<ObfuscatedEmail user={e.user} domain={e.domain} className="hover:text-[#7DD3FC] block truncate" />
|
user={e.user}
|
||||||
</div>
|
domain={e.domain}
|
||||||
);
|
className="text-[#aab1bd] hover:text-[#7DD3FC] font-mono block"
|
||||||
})()}
|
/>
|
||||||
{p.phone && (
|
);
|
||||||
<div>
|
})()
|
||||||
<a href={`tel:${p.phone.replace(/[^\d+]/g, '')}${p.extension ? ',' + p.extension : ''}`} className="hover:text-[#7DD3FC] font-mono">
|
) : (
|
||||||
{formatPhone(p.phone)}{p.extension ? `, Ext. ${p.extension}` : ''}
|
<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>
|
</a>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<span className="text-[#6c7686]">—</span>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</td>
|
||||||
</li>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
@@ -275,4 +270,4 @@ export default async function TeamPage() {
|
|||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user