Files
avbeat-com/src/components/EventsDropdown.tsx
Ryan Salazar d67a2bd48d AV Beat rebrand: theme swap (orange #E67E22 / near-black), logo, schema default → avb
- public/assets/images/logo.png + public/brand/logo*.png replaced with the 1320x310 AV Beat logo
- public/assets/logos/avbeat.png added as the canonical source
- tailwind.config.js + src/styles/tailwind.css + src/styles/redesign-tokens.css palette swapped (primary #0F0E0E, secondary #1D1A1A, accent #E67E22 orange, foreground #F0EBE6 warm white)
- Layout / robots / sitemap / home-page / about / contact / press-kit / team / global-error metadata switched to AV Beat (Where AV Meets IT)
- Bulk replaced 'Broadcast Beat'/'BroadcastBeat.com'/'broadcastbeat.com' across user-facing news/forum/marketplace/advertise/search/newsletter pages
- src/lib/supabase/{admin,server,client} now default to schema 'avb' (was 'bb') — overridable via NEXT_PUBLIC_SUPABASE_SCHEMA
- package.json renamed to 'avbeat'
- Single d60701 reference in about/team swapped to E67E22

Design-only replica — no article content migrated.
2026-06-01 15:30:16 +00:00

191 lines
7.3 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.
"use client";
import { useEffect, useRef, useState } from "react";
import Link from "next/link";
interface EventRow {
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;
hashtag: string | null;
is_live: boolean;
days_until: number;
day_of_event: number | null;
total_days: number;
}
function fmtDateRange(s: string, e: string): string {
const a = new Date(s);
const b = new Date(e);
const sameMonth = a.getUTCMonth() === b.getUTCMonth() && a.getUTCFullYear() === b.getUTCFullYear();
const mFmt = (d: Date) => d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
const yFmt = (d: Date) => d.getUTCFullYear();
if (sameMonth) {
return `${mFmt(a)}${b.getUTCDate()}, ${yFmt(b)}`;
}
return `${mFmt(a)} ${mFmt(b)}, ${yFmt(b)}`;
}
function hostFromUrl(u: string | null): string {
if (!u) return "";
try {
return new URL(u).hostname.replace(/^www\./, "");
} catch {
return "";
}
}
function ago(iso: string): string {
const t = Date.now() - new Date(iso).getTime();
const m = Math.floor(t / 60000);
if (m < 1) return "just now";
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h ago`;
return `${Math.floor(h / 24)}d ago`;
}
export default function EventsDropdown() {
const [open, setOpen] = useState(false);
const [events, setEvents] = useState<EventRow[]>([]);
const [loadedAt, setLoadedAt] = useState<string>("");
const [error, setError] = useState(false);
const wrapRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let alive = true;
fetch("/api/events/upcoming")
.then((r) => r.json())
.then((d) => {
if (!alive) return;
setEvents(d.events || []);
setLoadedAt(d.generated_at || new Date().toISOString());
})
.catch(() => alive && setError(true));
return () => {
alive = false;
};
}, []);
useEffect(() => {
if (!open) return;
const onClick = (e: MouseEvent) => {
if (!wrapRef.current) return;
if (!wrapRef.current.contains(e.target as Node)) setOpen(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
document.addEventListener("mousedown", onClick);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onClick);
document.removeEventListener("keydown", onKey);
};
}, [open]);
return (
<div ref={wrapRef} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-haspopup="true"
aria-expanded={open}
className="bb-browse-item gap-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]"
>
<span>Show coverage</span>
<span aria-hidden="true" className="text-[10px]"></span>
</button>
{open && (
<div
role="menu"
aria-label="Upcoming industry events"
className="absolute left-0 top-full mt-1 w-[340px] z-50 bg-[var(--color-background-primary,#0a0a0a)] border border-[#252525] text-[#e5e7eb] shadow-xl"
style={{ borderRadius: "var(--border-radius-md,6px)" }}
>
<div className="relative p-4">
<span aria-hidden className="absolute top-2 left-2 w-3 h-3 border-t border-l border-[var(--color-text-info,#E67E22)]"></span>
<span aria-hidden className="absolute bottom-2 right-2 w-3 h-3 border-b border-r border-[var(--color-text-info,#E67E22)]"></span>
<div className="flex items-center justify-between mb-1">
<h3 className="font-serif text-base font-semibold">Show coverage</h3>
<span className="inline-flex items-center gap-1 text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#E67E22)]">
<span className="bb-pulse-dot" />
BB AI
</span>
</div>
<div className="text-[11px] font-mono text-[#6b7280] mb-3">
upcoming · chronological
</div>
<ul className="space-y-2 max-h-[60vh] overflow-y-auto pr-1">
{events.length === 0 && !error && (
<li className="text-xs text-[#6b7280]">Loading</li>
)}
{error && (
<li className="text-xs text-red-400">Couldn't load events.</li>
)}
{events.map((e) => {
const host = hostFromUrl(e.url);
return (
<li key={e.slug}>
<Link
href={`/show-coverage/${e.slug}`}
role="menuitem"
className="block px-2 py-2 rounded hover:bg-[var(--color-background-tertiary,#1a1f2e)] focus:outline-none focus-visible:bg-[var(--color-background-tertiary,#1a1f2e)] transition-colors"
onClick={() => setOpen(false)}
>
<div className="flex items-center gap-2 text-[11px] font-mono">
{e.is_live ? (
<>
<span className="bb-pulse-dot bb-pulse-dot--red" />
<span className="text-red-400 uppercase tracking-wider">
live · {e.day_of_event === e.total_days ? "final day" : `day ${e.day_of_event} of ${e.total_days}`}
</span>
</>
) : (
<>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#6b7280]" aria-hidden="true">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
<span className="text-[var(--color-text-info,#E67E22)]">T-{e.days_until}d</span>
</>
)}
</div>
<div className="font-serif text-sm mt-0.5">{e.name}</div>
<div className="text-xs text-[#9ca3af] mt-0.5">
{[e.venue, e.city].filter(Boolean).join(" · ")}
</div>
<div className="flex items-center gap-2 text-[10px] font-mono text-[#6b7280] mt-1">
<span>{fmtDateRange(e.start_date, e.end_date)}</span>
{host && <span className="text-[var(--color-text-info,#E67E22)]">· {host}</span>}
</div>
</Link>
</li>
);
})}
</ul>
<div className="mt-3 pt-2 border-t border-[#252525] flex items-center justify-between text-[10px] font-mono text-[#6b7280]">
<Link href="/show-coverage" className="text-[var(--color-text-info,#E67E22)] hover:underline" onClick={() => setOpen(false)}>
See all 13 events
</Link>
{loadedAt && <span>auto-updated · {ago(loadedAt)}</span>}
</div>
</div>
</div>
)}
</div>
);
}