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); }