From a9823b1d8ac08fa7509c4c34173122eb253d73b1 Mon Sep 17 00:00:00 2001 From: Ryan Salazar Date: Wed, 20 May 2026 07:02:15 +0000 Subject: [PATCH] ticker: replace LiveWire with dual-row neon ticker (news + events) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Row 1 (Live Wire, scrolls →): most recent imported + AI-rewritten articles, neon-green label, monospaced [HH:MM] [SOURCE_CODE] prefix. Row 2 (On Calendar, scrolls ←, slower): upcoming events from bb.events (confirmed/tentative, start_date >= today), neon-cyan label, [Mon DD] [SHORT_NAME] prefix. Defensive: each query is in its own try/catch so a partial failure renders an empty row rather than crashing the page — that was the silent build crash that took down the prior DoubleTicker incarnation. If BOTH queries return zero rows we render nothing instead of two blank bars. CSS tokens added to redesign-tokens.css: - bb-marquee--reverse + bb-marquee--slow modifiers - .bb-neon-bar / .bb-neon-label / .bb-neon-time / .bb-neon-code (scoped class names so this is the ONLY place on the site that uses the neon palette — rest of the site keeps dark-navy + blue) User cancelled the wholesale neon redesign; only the dual-ticker keeps the neon green/cyan styling. --- public/previews/neon.html | 212 -------------------------------- src/app/home-page/page.tsx | 4 +- src/components/DoubleTicker.tsx | 205 ++++++++++++++++++++++++++++++ src/styles/redesign-tokens.css | 40 ++++++ 4 files changed, 247 insertions(+), 214 deletions(-) delete mode 100644 public/previews/neon.html create mode 100644 src/components/DoubleTicker.tsx diff --git a/public/previews/neon.html b/public/previews/neon.html deleted file mode 100644 index 5f15079..0000000 --- a/public/previews/neon.html +++ /dev/null @@ -1,212 +0,0 @@ - - - - - -Broadcast Beat — Neon Redesign Preview - - - - -
Preview — Neon Redesign · NOT live · review only
- - - -
-
- LIVE WIRE - NAB 2027 LAS VEGAS · APR 11 - | - IBC 2026 AMSTERDAM · SEP 11 - | - 26,103 ARTICLES INDEXED - | - v2.0.0 -
-
- -
-
-

Broadcast Engineering, Reimagined

-

Real-time insights, cutting-edge tech, vibrant community.

-

The digital platform for broadcast engineering professionals. Breaking news, deep-dive features, and industry spotlights from NAB to IBC and beyond.

- -
-
- -
-
-

Industry News

-

Latest from the broadcast technology beat.

-
- -
-
FEATURED IMAGE
-
-
Streaming & IP
-

Telestream Expands Its Cloud Services with the Introduction of UP

-

New cloud-native platform extends Telestream Cloud Services to support Global Ingest, automation, review, and real-time monitoring.

-
Ryan Salazar · 4 min read
-
-
- -
-
FEATURED IMAGE
-
-
Live Production
-

Emergent Launches Fusion: The Interactive Anything Platform

-

No-code application builder designed to transform complex live data into immersive interactive storytelling experiences.

-
Ryan Salazar · 3 min read
-
-
- -
-
FEATURED IMAGE
-
-
Audio
-

Calrec Audio Unveils Brio Duet Console for Mid-Tier Broadcast Studios

-

A compact mid-tier mixing solution targeting regional broadcasters and corporate AV deployments.

-
Ryan Salazar · 5 min read
-
-
- -
-
FEATURED IMAGE
-
-
Cloud & Workflow
-

GlobalM Introduces Distributed Video Gateway Architecture

-

New distributed architecture addresses the growing demand for scalable, resilient live IP transport solutions.

-
Ryan Salazar · 3 min read
-
-
- -
-
FEATURED IMAGE
-
-
Show Coverage
-

Viaccess-Orca Brings Efficiency to NAB 2026

-

Purpose-driven solutions designed to help broadcasters reduce operational costs while improving content monetization.

-
Ryan Salazar · 4 min read
-
-
- -
-
FEATURED IMAGE
-
-
Cameras & Capture
-

Sony BRC-AM7 + HXC-FZ90 Launch Reshapes Studio PTZ Workflows

-

Two new system cameras anchor Sony's spring broadcast announcement, targeting houses of worship and corporate live.

-
Ryan Salazar · 6 min read
-
-
- -
-
-
- - - - - diff --git a/src/app/home-page/page.tsx b/src/app/home-page/page.tsx index b179687..2a8d21b 100644 --- a/src/app/home-page/page.tsx +++ b/src/app/home-page/page.tsx @@ -4,7 +4,7 @@ import Header from "@/components/Header"; import Footer from "@/components/Footer"; import FeaturedBentoFromDb from '@/components/FeaturedBentoFromDb'; import FeaturedCarouselServer from '@/components/FeaturedCarouselServer'; -import LiveWireTicker from '@/components/LiveWireTicker'; +import DoubleTicker from '@/components/DoubleTicker'; import NewsTicker from "./components/NewsTicker"; import FeaturedBento from "./components/FeaturedBento"; import SpotlightCarousel from "./components/SpotlightCarousel"; @@ -150,7 +150,7 @@ export default function HomePage() { {/* Scrolling news ticker */}
}> - +
diff --git a/src/components/DoubleTicker.tsx b/src/components/DoubleTicker.tsx new file mode 100644 index 0000000..5632542 --- /dev/null +++ b/src/components/DoubleTicker.tsx @@ -0,0 +1,205 @@ +import Link from "next/link"; +import { createClient } from "@supabase/supabase-js"; + +export const dynamic = "force-dynamic"; + +interface NewsItem { + slug: string; + title: string; + published_at: string; + source_code: string; +} + +interface EventItem { + slug: string | null; + short_name: string; + name: string; + start_date: string; + city: string | null; + country: string | null; +} + +const SOURCE_CODES: { match: RegExp; code: string }[] = [ + { match: /blackmagic|davinci/i, code: "BMD" }, + { match: /magewell/i, code: "MGW" }, + { match: /aja/i, code: "AJA" }, + { match: /sony/i, code: "SONY" }, + { match: /matrox/i, code: "MTRX" }, + { match: /telestream/i, code: "TLST" }, + { match: /grass valley/i, code: "GV" }, + { match: /lawo/i, code: "LAWO" }, + { match: /calrec/i, code: "CLRC" }, + { match: /zixi/i, code: "ZIXI" }, + { match: /liveu/i, code: "LVU" }, + { match: /netflix/i, code: "NFLX" }, + { match: /youtube/i, code: "YT" }, + { match: /amazon|aws/i, code: "AWS" }, +]; + +function codeFor(title: string): string { + for (const { match, code } of SOURCE_CODES) if (match.test(title)) return code; + return "WIRE"; +} + +function hhmm(iso: string): string { + const d = new Date(iso); + const h = String(d.getUTCHours()).padStart(2, "0"); + const m = String(d.getUTCMinutes()).padStart(2, "0"); + return `${h}:${m}`; +} + +function fmtEventDate(iso: string): string { + // Show "MMM DD" (no year) so each row stays compact. + try { + return new Date(iso).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + timeZone: "UTC", + }); + } catch { + return iso.slice(5, 10); + } +} + +// Server-side fetches return arrays — but a network/RLS error can throw. +// Each is wrapped so a partial failure renders an empty row rather than +// crashing the whole component (the silent build crash that took down +// the prior incarnation of this component was caused by an uncaught +// throw inside an async server component). +async function loadNews(sb: any): Promise { + try { + const [wp, ai] = await Promise.all([ + sb + .from("wp_imported_posts") + .select("wp_slug, title, wp_published_at") + .eq("status", "published") + .order("wp_published_at", { ascending: false }) + .limit(12), + sb + .from("ai_rewritten_articles") + .select("slug, title, published_at") + .eq("status", "published") + .order("published_at", { ascending: false, nullsFirst: false }) + .limit(6), + ]); + const items: NewsItem[] = []; + for (const r of (wp.data || []) as any[]) { + items.push({ + slug: r.wp_slug, + title: r.title, + published_at: r.wp_published_at, + source_code: codeFor(r.title || ""), + }); + } + for (const r of (ai.data || []) as any[]) { + items.push({ + slug: r.slug, + title: r.title, + published_at: r.published_at, + source_code: codeFor(r.title || ""), + }); + } + items.sort( + (a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime() + ); + return items.slice(0, 14); + } catch { + return []; + } +} + +async function loadEvents(sb: any): Promise { + try { + const { data } = await sb + .from("events") + .select("slug, short_name, name, start_date, city, country") + .in("status", ["confirmed", "tentative"]) + .gte("start_date", new Date().toISOString().slice(0, 10)) + .order("start_date", { ascending: true }) + .limit(12); + return ((data || []) as any[]).map((r) => ({ + slug: r.slug || null, + short_name: r.short_name || r.name, + name: r.name, + start_date: r.start_date, + city: r.city, + country: r.country, + })); + } catch { + return []; + } +} + +export default async function DoubleTicker() { + const sb = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + db: { schema: (process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb") as "public" }, + auth: { persistSession: false }, + } + ); + + const [news, events] = await Promise.all([loadNews(sb), loadEvents(sb)]); + + // If both queries failed, render nothing — better than a half-blank bar. + if (news.length === 0 && events.length === 0) return null; + + return ( +
+ {/* Row 1 — Live wire (news, right-to-left, fast). */} + {news.length > 0 && ( +
+
+ Live Wire +
+
+ {[...news, ...news].map((it, i) => ( + + [{hhmm(it.published_at)}] + [{it.source_code}] + {it.title} + + ))} +
+
+
+
+ )} + + {/* Row 2 — Industry calendar (events, left-to-right, slower). */} + {events.length > 0 && ( +
+
+ On Calendar +
+
+ {[...events, ...events].map((ev, i) => { + const where = [ev.city, ev.country].filter(Boolean).join(", "); + const inner = ( + + [{fmtEventDate(ev.start_date)}] + [{ev.short_name}] + {ev.name}{where ? ` — ${where}` : ""} + + ); + return ev.slug ? ( + + {inner} + + ) : ( + {inner} + ); + })} +
+
+
+
+ )} +
+ ); +} diff --git a/src/styles/redesign-tokens.css b/src/styles/redesign-tokens.css index 7ed1aef..028f5da 100644 --- a/src/styles/redesign-tokens.css +++ b/src/styles/redesign-tokens.css @@ -74,9 +74,49 @@ from { transform: translateX(0); } to { transform: translateX(-50%); } } +@keyframes bb-marquee-reverse { + from { transform: translateX(-50%); } + to { transform: translateX(0); } +} .bb-marquee { display: flex; white-space: nowrap; animation: bb-marquee 90s linear infinite; } .bb-marquee:hover { animation-play-state: paused; } +.bb-marquee--reverse { + animation-name: bb-marquee-reverse; +} +.bb-marquee--slow { + animation-duration: 140s; +} + +/* Neon dual-ticker tokens — used ONLY inside DoubleTicker. */ +.bb-neon-bar { + background: linear-gradient(180deg, rgba(0,0,0,0.55), rgba(5,10,8,0.55)); + border-top: 1px solid rgba(0,255,159,0.18); + border-bottom: 1px solid rgba(0,255,159,0.18); +} +.bb-neon-bar + .bb-neon-bar { + border-top: 1px solid rgba(0,212,255,0.10); /* faint divider between the two */ +} +.bb-neon-label { + background: #00ff9f; + color: #050a08; + font-weight: 700; + font-size: 10px; + letter-spacing: 0.15em; + padding: 2px 8px; + border-radius: 3px; + text-transform: uppercase; + flex-shrink: 0; + text-shadow: none; +} +.bb-neon-label--cyan { + background: #00d4ff; +} +.bb-neon-time { color: #707070; font-family: 'IBM Plex Mono', ui-monospace, monospace; } +.bb-neon-code { color: #00ff9f; font-family: 'IBM Plex Mono', ui-monospace, monospace; } +.bb-neon-code--cyan { color: #00d4ff; } +.bb-neon-text { color: #d4d4d4; } +.bb-neon-text:hover { color: #00ff9f; text-shadow: 0 0 8px rgba(0,255,159,0.4); }