ticker: replace LiveWire with dual-row neon ticker (news + events)

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.
This commit is contained in:
Ryan Salazar
2026-05-20 07:02:15 +00:00
parent 5c3432915a
commit a9823b1d8a
4 changed files with 247 additions and 214 deletions

View File

@@ -1,212 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Broadcast Beat — Neon Redesign Preview</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&display=swap');
:root{
--neon-primary:#00ff9f;--neon-primary-light:#33ffb3;--neon-secondary:#00d4ff;--neon-accent:#c400ff;
--bg-dark:#050a08;--bg-darker:#030605;--bg-card:#0a1410;
--text-primary:#f0f0f0;--text-secondary:#b0b0b0;--text-muted:#707070;
--space-sm:1rem;--space-md:1.5rem;--space-lg:2rem;--space-xl:3rem;--space-2xl:4rem;
--radius-md:8px;--radius-lg:12px;--transition:all 0.3s cubic-bezier(0.4,0,0.2,1)
}
*{transition:var(--transition);-webkit-font-smoothing:antialiased;box-sizing:border-box}
html{font-size:16px;scroll-behavior:smooth}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Roboto','Helvetica Neue',sans-serif;background:var(--bg-dark);color:var(--text-primary);line-height:1.6;margin:0}
h1,h2,h3,h4,h5,h6{font-family:'Space Grotesk',sans-serif;font-weight:700;line-height:1.2;margin:0 0 1rem 0;letter-spacing:0.05em}
h1{font-size:clamp(2rem,8vw,3.5rem);background:linear-gradient(135deg,var(--neon-primary),var(--neon-secondary));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;text-transform:uppercase;letter-spacing:0.1em;margin-bottom:1.5rem}
h2{font-size:clamp(1.5rem,5vw,2.5rem);color:var(--text-primary);padding-bottom:0.75rem;position:relative}
h2::after{content:'';position:absolute;bottom:0;left:0;width:60px;height:3px;background:linear-gradient(90deg,var(--neon-primary),var(--neon-secondary))}
h3{font-size:1.5rem;color:var(--neon-secondary)}
p{margin:0 0 1.5rem 0}
p.lead{font-size:1.25rem;color:var(--text-secondary);line-height:1.8}
strong,b{font-weight:700;color:var(--neon-primary)}
a{color:var(--neon-primary);text-decoration:none;position:relative}
a:hover{color:var(--neon-primary-light);text-shadow:0 0 10px rgba(0,255,159,0.5)}
nav.navbar{background:linear-gradient(180deg,rgba(0,0,0,0.9),rgba(5,10,8,0.85));border-bottom:2px solid rgba(0,255,159,0.2);padding:var(--space-md) var(--space-lg);position:sticky;top:0;z-index:1000;backdrop-filter:blur(10px)}
.navbar-container{max-width:1400px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:var(--space-lg)}
.nav-logo{font-size:1.5rem;font-weight:700;background:linear-gradient(135deg,var(--neon-primary),var(--neon-secondary));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;text-transform:uppercase;letter-spacing:0.1em}
.nav-menu{display:flex;gap:var(--space-lg);list-style:none;margin:0;padding:0}
.nav-item a{color:var(--text-primary);padding:var(--space-sm) var(--space-md);text-transform:uppercase;font-size:0.9rem;letter-spacing:0.08em;font-weight:500}
.nav-item a:hover{color:var(--neon-primary)}
.hero{min-height:60vh;display:flex;align-items:center;justify-content:center;padding:var(--space-2xl) var(--space-lg);position:relative;overflow:hidden;background:linear-gradient(135deg,var(--bg-dark) 0%,var(--bg-darker) 50%,rgba(0,255,159,0.04) 100%)}
.hero::before{content:'';position:absolute;top:-50%;right:-50%;width:200%;height:200%;background:radial-gradient(circle,rgba(0,255,159,0.10) 0%,transparent 70%);animation:float 20s ease-in-out infinite;pointer-events:none}
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(50px,50px)}}
.hero-content{position:relative;z-index:2;text-align:center;max-width:900px}
.hero .tagline{font-size:clamp(1.1rem,3vw,1.5rem);color:var(--neon-secondary);margin-bottom:var(--space-lg);font-weight:500}
.cta-group{display:flex;gap:var(--space-md);justify-content:center;flex-wrap:wrap;margin-top:var(--space-xl)}
.cta-button{padding:var(--space-md) var(--space-lg);font-size:1.05rem;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;border-radius:var(--radius-lg);cursor:pointer;border:none;text-decoration:none;display:inline-block}
.cta-button.primary{background:linear-gradient(135deg,var(--neon-primary),var(--neon-secondary));color:var(--bg-dark)}
.cta-button.primary:hover{transform:translateY(-3px);box-shadow:0 20px 40px rgba(0,255,159,0.4)}
.cta-button.secondary{background:transparent;border:2px solid var(--neon-primary);color:var(--neon-primary)}
.cta-button.secondary:hover{background:rgba(0,255,159,0.1);box-shadow:0 0 20px rgba(0,255,159,0.3)}
.container{max-width:1400px;margin:0 auto;padding:var(--space-lg)}
section{padding:var(--space-2xl) var(--space-lg);border-top:1px solid rgba(0,255,159,0.1)}
section:first-of-type{border-top:none}
.grid{display:grid;gap:var(--space-lg);grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
.card{background:linear-gradient(135deg,rgba(10,20,16,0.8),rgba(15,22,18,0.8));border:1px solid rgba(0,255,159,0.15);border-radius:var(--radius-lg);padding:0;position:relative;backdrop-filter:blur(10px);overflow:hidden}
.card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--neon-primary),transparent);opacity:0.5}
.card:hover{border-color:var(--neon-primary);box-shadow:0 10px 30px rgba(0,255,159,0.12);transform:translateY(-6px)}
.card-img{width:100%;height:160px;background:linear-gradient(135deg,#0a1a14,#0e2a20);position:relative;display:flex;align-items:center;justify-content:center;color:rgba(0,255,159,0.25);font-size:0.7rem;letter-spacing:0.15em;text-transform:uppercase}
.card-body{padding:var(--space-md)}
.card-cat{font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:0.18em;color:var(--neon-primary);margin-bottom:0.5rem}
.card-title{font-family:'Space Grotesk',sans-serif;font-size:1.05rem;font-weight:700;color:var(--text-primary);margin:0 0 0.5rem 0;line-height:1.3}
.card-excerpt{font-size:0.85rem;color:var(--text-secondary);line-height:1.55;margin:0 0 0.75rem 0}
.card-meta{font-size:0.7rem;color:var(--text-muted);letter-spacing:0.06em}
.ticker{background:rgba(0,0,0,0.4);border-top:1px solid rgba(0,255,159,0.15);border-bottom:1px solid rgba(0,255,159,0.15);padding:0.6rem 0;font-size:0.8rem;color:var(--text-secondary);font-family:'IBM Plex Mono','SF Mono',monospace;letter-spacing:0.05em}
.ticker-content{max-width:1400px;margin:0 auto;padding:0 var(--space-lg);display:flex;align-items:center;gap:1.5rem;overflow:hidden;white-space:nowrap}
.ticker-label{background:var(--neon-primary);color:var(--bg-dark);padding:0.15rem 0.5rem;font-weight:700;font-size:0.65rem;letter-spacing:0.15em;border-radius:3px;flex-shrink:0}
footer{background:linear-gradient(180deg,rgba(0,0,0,0.5),rgba(5,10,8,0.8));border-top:2px solid var(--neon-primary);padding:var(--space-xl) var(--space-lg);margin-top:var(--space-2xl);color:var(--text-secondary)}
.footer-container{max-width:1400px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--space-lg)}
.footer-h{color:var(--neon-secondary);font-family:'Space Grotesk',sans-serif;font-size:0.85rem;text-transform:uppercase;letter-spacing:0.12em;margin-bottom:0.75rem}
footer a{color:var(--text-secondary);display:block;padding:0.2rem 0;font-size:0.85rem}
footer a:hover{color:var(--neon-primary)}
.preview-banner{background:linear-gradient(90deg,rgba(196,0,255,0.15),rgba(0,212,255,0.15));border-bottom:1px solid var(--neon-accent);text-align:center;padding:0.6rem;font-size:0.8rem;color:var(--neon-accent);font-weight:600;letter-spacing:0.1em;text-transform:uppercase}
@media (max-width:768px){.container,section{padding:var(--space-md)}.grid{grid-template-columns:1fr}.cta-group{flex-direction:column}.cta-button{width:100%}.nav-menu{display:none}}
</style>
</head>
<body>
<div class="preview-banner">Preview — Neon Redesign · NOT live · review only</div>
<nav class="navbar">
<div class="navbar-container">
<div class="nav-logo">Broadcast Beat</div>
<ul class="nav-menu">
<li class="nav-item"><a href="#">News</a></li>
<li class="nav-item"><a href="#">Show Coverage</a></li>
<li class="nav-item"><a href="#">Technology</a></li>
<li class="nav-item"><a href="#">Gear</a></li>
<li class="nav-item"><a href="#">Forum</a></li>
</ul>
</div>
</nav>
<div class="ticker">
<div class="ticker-content">
<span class="ticker-label">LIVE WIRE</span>
<span>NAB 2027 LAS VEGAS · APR 11</span>
<span style="color:var(--text-muted)">|</span>
<span>IBC 2026 AMSTERDAM · SEP 11</span>
<span style="color:var(--text-muted)">|</span>
<span>26,103 ARTICLES INDEXED</span>
<span style="color:var(--text-muted)">|</span>
<span>v2.0.0</span>
</div>
</div>
<section class="hero">
<div class="hero-content">
<h1>Broadcast Engineering, Reimagined</h1>
<p class="tagline">Real-time insights, cutting-edge tech, vibrant community.</p>
<p class="lead">The digital platform for broadcast engineering professionals. Breaking news, deep-dive features, and industry spotlights from NAB to IBC and beyond.</p>
<div class="cta-group">
<a href="#" class="cta-button primary">Explore News</a>
<a href="#" class="cta-button secondary">Join the Crew Lounge</a>
</div>
</div>
</section>
<section>
<div class="container">
<h2>Industry News</h2>
<p style="color:var(--text-secondary);margin-bottom:var(--space-xl)">Latest from the broadcast technology beat.</p>
<div class="grid">
<article class="card">
<div class="card-img">FEATURED IMAGE</div>
<div class="card-body">
<div class="card-cat">Streaming &amp; IP</div>
<h3 class="card-title">Telestream Expands Its Cloud Services with the Introduction of UP</h3>
<p class="card-excerpt">New cloud-native platform extends Telestream Cloud Services to support Global Ingest, automation, review, and real-time monitoring.</p>
<div class="card-meta">Ryan Salazar · 4 min read</div>
</div>
</article>
<article class="card">
<div class="card-img">FEATURED IMAGE</div>
<div class="card-body">
<div class="card-cat">Live Production</div>
<h3 class="card-title">Emergent Launches Fusion: The Interactive Anything Platform</h3>
<p class="card-excerpt">No-code application builder designed to transform complex live data into immersive interactive storytelling experiences.</p>
<div class="card-meta">Ryan Salazar · 3 min read</div>
</div>
</article>
<article class="card">
<div class="card-img">FEATURED IMAGE</div>
<div class="card-body">
<div class="card-cat">Audio</div>
<h3 class="card-title">Calrec Audio Unveils Brio Duet Console for Mid-Tier Broadcast Studios</h3>
<p class="card-excerpt">A compact mid-tier mixing solution targeting regional broadcasters and corporate AV deployments.</p>
<div class="card-meta">Ryan Salazar · 5 min read</div>
</div>
</article>
<article class="card">
<div class="card-img">FEATURED IMAGE</div>
<div class="card-body">
<div class="card-cat">Cloud &amp; Workflow</div>
<h3 class="card-title">GlobalM Introduces Distributed Video Gateway Architecture</h3>
<p class="card-excerpt">New distributed architecture addresses the growing demand for scalable, resilient live IP transport solutions.</p>
<div class="card-meta">Ryan Salazar · 3 min read</div>
</div>
</article>
<article class="card">
<div class="card-img">FEATURED IMAGE</div>
<div class="card-body">
<div class="card-cat">Show Coverage</div>
<h3 class="card-title">Viaccess-Orca Brings Efficiency to NAB 2026</h3>
<p class="card-excerpt">Purpose-driven solutions designed to help broadcasters reduce operational costs while improving content monetization.</p>
<div class="card-meta">Ryan Salazar · 4 min read</div>
</div>
</article>
<article class="card">
<div class="card-img">FEATURED IMAGE</div>
<div class="card-body">
<div class="card-cat">Cameras &amp; Capture</div>
<h3 class="card-title">Sony BRC-AM7 + HXC-FZ90 Launch Reshapes Studio PTZ Workflows</h3>
<p class="card-excerpt">Two new system cameras anchor Sony's spring broadcast announcement, targeting houses of worship and corporate live.</p>
<div class="card-meta">Ryan Salazar · 6 min read</div>
</div>
</article>
</div>
</div>
</section>
<footer>
<div class="footer-container">
<div>
<div class="footer-h">Broadcast Beat</div>
<p style="font-size:0.85rem;color:var(--text-muted)">The digital platform for broadcast engineering professionals.</p>
</div>
<div>
<div class="footer-h">Sections</div>
<a href="#">Industry News</a>
<a href="#">Show Coverage</a>
<a href="#">Production Technology</a>
<a href="#">Forum</a>
</div>
<div>
<div class="footer-h">Network</div>
<a href="#">Relevant Media Properties</a>
<a href="#">AV Beat</a>
<a href="#">The Crew Grid</a>
</div>
<div>
<div class="footer-h">Contact</div>
<a href="#">Advertise</a>
<a href="#">Editorial</a>
<a href="#">About</a>
</div>
</div>
</footer>
</body>
</html>

View File

@@ -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 */}
<div className="section-enter section-enter-1">
<Suspense fallback={<TickerSkeleton />}>
<LiveWireTicker />
<DoubleTicker />
</Suspense>
</div>

View File

@@ -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<NewsItem[]> {
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<EventItem[]> {
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 (
<div role="region" aria-label="Live wire and upcoming events">
{/* Row 1 — Live wire (news, right-to-left, fast). */}
{news.length > 0 && (
<div className="bb-neon-bar w-full overflow-hidden">
<div className="max-w-container mx-auto px-4 h-[32px] flex items-center gap-3 text-[12px]">
<span className="bb-neon-label">Live Wire</span>
<div className="overflow-hidden flex-1">
<div className="bb-marquee">
{[...news, ...news].map((it, i) => (
<Link
key={`n-${it.slug}-${i}`}
href={`/news/${it.slug}`}
className="inline-flex items-center gap-2 px-4 whitespace-nowrap bb-neon-text"
>
<span className="bb-neon-time">[{hhmm(it.published_at)}]</span>
<span className="bb-neon-code">[{it.source_code}]</span>
<span>{it.title}</span>
</Link>
))}
</div>
</div>
</div>
</div>
)}
{/* Row 2 — Industry calendar (events, left-to-right, slower). */}
{events.length > 0 && (
<div className="bb-neon-bar w-full overflow-hidden">
<div className="max-w-container mx-auto px-4 h-[32px] flex items-center gap-3 text-[12px]">
<span className="bb-neon-label bb-neon-label--cyan">On Calendar</span>
<div className="overflow-hidden flex-1">
<div className="bb-marquee bb-marquee--reverse bb-marquee--slow">
{[...events, ...events].map((ev, i) => {
const where = [ev.city, ev.country].filter(Boolean).join(", ");
const inner = (
<span className="inline-flex items-center gap-2 px-4 whitespace-nowrap bb-neon-text">
<span className="bb-neon-time">[{fmtEventDate(ev.start_date)}]</span>
<span className="bb-neon-code bb-neon-code--cyan">[{ev.short_name}]</span>
<span>{ev.name}{where ? `${where}` : ""}</span>
</span>
);
return ev.slug ? (
<Link key={`e-${ev.short_name}-${i}`} href={`/events/${ev.slug}`}>
{inner}
</Link>
) : (
<span key={`e-${ev.short_name}-${i}`}>{inner}</span>
);
})}
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

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