"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([]); const [loadedAt, setLoadedAt] = useState(""); const [error, setError] = useState(false); const wrapRef = useRef(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 (
{open && (

Show coverage

BB AI
upcoming · chronological
    {events.length === 0 && !error && (
  • Loading…
  • )} {error && (
  • Couldn't load events.
  • )} {events.map((e) => { const host = hostFromUrl(e.url); return (
  • setOpen(false)} >
    {e.is_live ? ( <> live · {e.day_of_event === e.total_days ? "final day" : `day ${e.day_of_event} of ${e.total_days}`} ) : ( <> T-{e.days_until}d )}
    {e.name}
    {[e.venue, e.city].filter(Boolean).join(" · ")}
    {fmtDateRange(e.start_date, e.end_date)} {host && · {host}}
  • ); })}
setOpen(false)}> See all 13 events → {loadedAt && auto-updated · {ago(loadedAt)}}
)}
); }