136 lines
4.6 KiB
TypeScript
136 lines
4.6 KiB
TypeScript
import Link from "next/link";
|
||
import { createClient } from "@supabase/supabase-js";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
export const revalidate = 600;
|
||
|
||
interface Row {
|
||
id: string;
|
||
slug: string;
|
||
name: string;
|
||
short_name: string | null;
|
||
start_date: string;
|
||
end_date: string;
|
||
venue: string | null;
|
||
city: string | null;
|
||
country: string | null;
|
||
url: string | null;
|
||
status: string;
|
||
category: string | null;
|
||
}
|
||
|
||
function fmtRange(s: string, e: string): string {
|
||
const a = new Date(s);
|
||
const b = new Date(e);
|
||
const sameYear = a.getUTCFullYear() === b.getUTCFullYear();
|
||
const sameMo = a.getUTCMonth() === b.getUTCMonth() && sameYear;
|
||
const f = (d: Date) =>
|
||
d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
||
if (sameMo) return `${f(a)}–${b.getUTCDate()}, ${b.getUTCFullYear()}`;
|
||
if (sameYear) return `${f(a)} – ${f(b)}, ${b.getUTCFullYear()}`;
|
||
return `${f(a)} ${a.getUTCFullYear()} – ${f(b)} ${b.getUTCFullYear()}`;
|
||
}
|
||
|
||
function pillStatus(s: string) {
|
||
const map: Record<string, string> = {
|
||
confirmed: "bg-emerald-500/10 text-emerald-400 border-emerald-500/30",
|
||
tentative: "bg-amber-500/10 text-amber-400 border-amber-500/30",
|
||
cancelled: "bg-zinc-500/10 text-zinc-400 border-zinc-500/30",
|
||
};
|
||
return `inline-flex items-center text-[10px] font-mono uppercase tracking-wider px-2 py-0.5 rounded-full border ${map[s] || map.tentative}`;
|
||
}
|
||
|
||
export default async function ShowCoverageIndex() {
|
||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||
const schema = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb";
|
||
|
||
const sb = createClient(url, key, {
|
||
db: { schema: schema as "public" },
|
||
auth: { persistSession: false },
|
||
});
|
||
|
||
const { data } = await sb
|
||
.from("events")
|
||
.select("id,slug,name,short_name,start_date,end_date,venue,city,country,url,status,category")
|
||
.in("status", ["confirmed", "tentative"])
|
||
.order("start_date", { ascending: true });
|
||
|
||
const events = (data || []) as Row[];
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
const upcoming = events.filter((e) => e.end_date >= today);
|
||
const past = events.filter((e) => e.end_date < today);
|
||
|
||
return (
|
||
<main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]">
|
||
<header className="mb-8">
|
||
<h1 className="font-serif text-4xl font-bold tracking-tight">Show coverage</h1>
|
||
<p className="text-sm text-[#9ca3af] mt-2 font-mono">
|
||
{upcoming.length} upcoming · {past.length} past · {events.length} total
|
||
</p>
|
||
</header>
|
||
|
||
<section className="mb-12">
|
||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-4">
|
||
Upcoming
|
||
</h2>
|
||
{upcoming.length === 0 ? (
|
||
<p className="text-sm text-[#6b7280]">No upcoming events.</p>
|
||
) : (
|
||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{upcoming.map((e) => (
|
||
<EventCard key={e.slug} e={e} />
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
|
||
{past.length > 0 && (
|
||
<section>
|
||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-4">
|
||
Past
|
||
</h2>
|
||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{past.map((e) => (
|
||
<EventCard key={e.slug} e={e} />
|
||
))}
|
||
</ul>
|
||
</section>
|
||
)}
|
||
</main>
|
||
);
|
||
}
|
||
|
||
function EventCard({ e }: { e: Row }) {
|
||
return (
|
||
<li>
|
||
<Link
|
||
href={`/show-coverage/${e.slug}`}
|
||
className="block p-5 rounded border border-[#252525] hover:border-[var(--color-text-info,#60a5fa)] bg-[#0b0f17] transition-colors"
|
||
>
|
||
<div className="flex items-start justify-between gap-3">
|
||
<h3 className="font-serif text-xl font-semibold">{e.name}</h3>
|
||
<span className={pillStatus(e.status)}>{e.status}</span>
|
||
</div>
|
||
<div className="text-sm text-[#9ca3af] mt-1">
|
||
{[e.venue, e.city, e.country].filter(Boolean).join(" · ")}
|
||
</div>
|
||
<div className="text-xs font-mono text-[#6b7280] mt-2">
|
||
{fmtRange(e.start_date, e.end_date)}
|
||
{e.url && (
|
||
<>
|
||
{" · "}
|
||
<span className="text-[var(--color-text-info,#60a5fa)]">{new URL(e.url).hostname.replace(/^www\./, "")}</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
</Link>
|
||
</li>
|
||
);
|
||
}
|
||
|
||
export const metadata = {
|
||
title: "Show coverage — industry events tracked by BroadcastBeat",
|
||
description: "All upcoming and past broadcast / production industry events tracked by BroadcastBeat, with linked coverage.",
|
||
};
|