trends sidebar, Ask BB AI, neural-summary schema, 13-event seed.
Phases 1-10 (11 phases minus Phase 8 which is the pgvector marker).
DB:
bb.events — 13 industry events seeded (MPTS, ANGA COM,
BroadcastAsia, SET Expo, IBC, NAB NY,
NewTECHForum, SATIS, DPP EBS, SVVS,
Hamburg Open, CABSAT, NAB Show '27).
bb.article_events — composite PK (article_id, article_table, event_id)
links any article-table row to one or more events.
bb.banner_creatives — (no change tonight)
bb.{native_articles,
ai_rewritten_articles,
wp_imported_posts,
press_releases}.neural_summary jsonb — populated by Phase 7
analyzer (deferred).
bb.{native_articles,
ai_rewritten_articles}.featured boolean — featured carousel source.
Routes:
/api/events/upcoming — date-gated, enriched with is_live/days_until/
day_of_event/total_days.
/api/ask-bb-ai — Claude Opus 4.7 chat with prompt caching on
the system block, 30-msg/hr/IP rate limit.
Tool-use deferred — first cut is straight
LLM with archive-citation prompting.
Pages:
/show-coverage — index of all events, upcoming + past split.
/show-coverage/[slug] — hero + status (live/T-Nd/past) + schema.org
Event JSON-LD + tagged articles list.
/about, /about/{team,contact,advertise,press-kit}.
Components:
Header — 4-item nav: Show coverage (dropdown) /
Newsletter / Forum / About (dropdown).
Old NEWS/GEAR/TECHNOLOGY/ADVERTISE items
removed from nav (routes still exist).
EventsDropdown — 340px CSI panel with L-corner brackets,
pulsing dot, "BB AI" badge, live/T-Nd rows,
auto-updated stamp.
AboutDropdown — 5-item lighter treatment.
SystemStatusBar — Index online pulse, articles/sources/events
counts, build version. Above Header.
LiveWireTicker — replaces NewsTicker. [HH:MM] [SRC] prefix,
CSS marquee, doubled for seamless loop.
TrendsSidebar — BB AI detected trends, top 5 entities by
7d-vs-30d lift, vector-pass stamp.
Phase 6b: replace with pgvector.
AskBBAI — floating bottom-right button + 420px right
drawer chat. No mention of Anthropic/Claude.
Styling:
redesign-tokens.css — --color-text-info, --color-background-*,
--font-serif/mono/body, bb-pulse/bb-ring
animations + bb-marquee.
Constraints honored:
- LiveU 728x90 still between </nav> and ticker.
- Blackmagic 300x600 still pinned top of sidebar.
- /r/[slug] click tracking unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.",
|
||
};
|