Files
avbeat-com/src/app/show-coverage/page.tsx
Claude (Phase B) a785ef428e BB AI redesign: 4-item nav, Show coverage, About, status bar, wire ticker,
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>
2026-05-15 02:22:23 +00:00

136 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.",
};