From a52f8adb21b8a0113a736f5727516082005f3e66 Mon Sep 17 00:00:00 2001 From: Ryan Salazar Date: Tue, 19 May 2026 23:06:15 +0000 Subject: [PATCH] home: replace single LiveWire ticker with stacked dual ticker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two horizontal lanes stacked on top of each other: Lane 1 LIVE WIRE red gradient label, pulsing white dot, latest 14 published articles, 65s loop Lane 2 RECENT FORUM POSTS green gradient label, pulsing white dot, latest 14 forum threads, 82s loop The two lanes use intentionally different animation durations (65s vs 82s) so they never sync up — feels alive like a real wire room. Both loops use CSS translateX with a duplicated content track for seamless infinite scroll. Edges fade via mask-image so items dissolve in/out rather than appearing on a hard line. Both lanes pause on hover (the whole row) so users can stop motion to read or click. Each item is a real ; clicking opens the article or thread page. Server component — data is fetched once at request time from bb.wp_imported_posts and bb.forum_threads, cached at the page's revalidate boundary. No client JS needed. Old LiveWireTicker.tsx is removed (orphaned, not referenced elsewhere). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/home-page/page.tsx | 6 +- src/components/DoubleTicker.tsx | 93 ++++++++++++++ src/components/LiveWireTicker.tsx | 118 ------------------ src/styles/tailwind.css | 195 +++++++++++++++++++++++++++++- 4 files changed, 290 insertions(+), 122 deletions(-) create mode 100644 src/components/DoubleTicker.tsx delete mode 100644 src/components/LiveWireTicker.tsx diff --git a/src/app/home-page/page.tsx b/src/app/home-page/page.tsx index b179687..5c0d830 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"; @@ -147,10 +147,10 @@ export default function HomePage() {

Broadcast Beat — Broadcast Engineering News & Insights

- {/* Scrolling news ticker */} + {/* Scrolling news + forum tickers */}
}> - +
diff --git a/src/components/DoubleTicker.tsx b/src/components/DoubleTicker.tsx new file mode 100644 index 0000000..a76a06f --- /dev/null +++ b/src/components/DoubleTicker.tsx @@ -0,0 +1,93 @@ +import Link from "next/link"; +import { createClient } from "@supabase/supabase-js"; + +export const dynamic = "force-dynamic"; + +interface TickerItem { + href: string; + title: string; +} + +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 [wireRes, forumRes] = await Promise.all([ + sb + .from("wp_imported_posts") + .select("wp_slug, title") + .eq("status", "published") + .order("wp_published_at", { ascending: false, nullsFirst: false }) + .limit(14), + sb + .from("forum_threads") + .select("id, title") + .order("last_reply_at", { ascending: false, nullsFirst: false }) + .limit(14), + ]); + + const wire: TickerItem[] = (wireRes.data || []).map((r: any) => ({ + href: `/news/${r.wp_slug}`, + title: r.title, + })); + const forum: TickerItem[] = (forumRes.data || []).map((r: any) => ({ + href: `/forum/thread/${r.id}`, + title: r.title, + })); + + // Fallback to keep layout sensible if either feed is empty + const wireItems = wire.length > 0 ? wire : [{ href: "/news", title: "Latest broadcast industry news loading…" }]; + const forumItems = forum.length > 0 ? forum : [{ href: "/forum", title: "Join the conversation in the forum" }]; + + return ( +
+ {/* LANE 1 — LIVE WIRE (red) */} +
+ + +
+
+ {[...wireItems, ...wireItems].map((item, i) => ( + + + {item.title} + + ))} +
+
+
+ + {/* LANE 2 — RECENT FORUM POSTS (teal-green) */} +
+ + +
+
+ {[...forumItems, ...forumItems].map((item, i) => ( + + + {item.title} + + ))} +
+
+
+
+ ); +} diff --git a/src/components/LiveWireTicker.tsx b/src/components/LiveWireTicker.tsx deleted file mode 100644 index 11d2235..0000000 --- a/src/components/LiveWireTicker.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import Link from "next/link"; -import { createClient } from "@supabase/supabase-js"; - -export const dynamic = "force-dynamic"; - -interface Item { - slug: string; - title: string; - published_at: string; - source_code: string; -} - -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}`; -} - -export default async function LiveWireTicker() { - 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 [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: Item[] = []; - 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() - ); - - const display = items.slice(0, 14); - - return ( -
-
- - Live wire - -
-
- {[...display, ...display].map((it, i) => ( - - [{hhmm(it.published_at)}] - [{it.source_code}] - {it.title} - - ))} -
-
-
-
- ); -} diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index 15f4bde..a07e74f 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -807,4 +807,197 @@ h1, h2, h3 { .ProseMirror strong { font-weight: 700; color: #fff; } .ProseMirror em { font-style: italic; } .ProseMirror u { text-decoration: underline; } -.ProseMirror s { text-decoration: line-through; } \ No newline at end of file +.ProseMirror s { text-decoration: line-through; } + +/* ===================== + DOUBLE TICKER (LIVE WIRE + RECENT FORUM POSTS) + ===================== */ +.bb-ticker-stack { + width: 100%; + border-bottom: 1px solid #1a1a1a; +} + +.bb-ticker { + display: flex; + align-items: stretch; + height: 34px; + width: 100%; + overflow: hidden; + position: relative; +} + +.bb-ticker--wire { + background: linear-gradient(90deg, #0a0a0a 0%, #161616 50%, #0a0a0a 100%); + border-bottom: 1px solid #1f1212; +} + +.bb-ticker--forum { + background: linear-gradient(90deg, #08120e 0%, #0f1d18 50%, #08120e 100%); +} + +.bb-ticker-label { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 16px; + font-family: var(--font-body); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.22em; + color: #ffffff; + white-space: nowrap; + position: relative; + z-index: 2; +} + +.bb-ticker-label--red { + background: linear-gradient(90deg, #d40000 0%, #a30000 100%); + box-shadow: 0 0 18px rgba(212, 0, 0, 0.45), inset 0 -1px 0 rgba(255, 255, 255, 0.05); + clip-path: polygon(0 0, 100% 0, calc(100% - 10px) 100%, 0 100%); + padding-right: 24px; +} + +.bb-ticker-label--green { + background: linear-gradient(90deg, #0c8a63 0%, #086149 100%); + box-shadow: 0 0 18px rgba(12, 138, 99, 0.40), inset 0 -1px 0 rgba(255, 255, 255, 0.05); + clip-path: polygon(0 0, 100% 0, calc(100% - 10px) 100%, 0 100%); + padding-right: 24px; +} + +.bb-ticker-pulse { + display: inline-block; + width: 7px; + height: 7px; + border-radius: 999px; + background: #ffffff; + box-shadow: 0 0 8px rgba(255, 255, 255, 0.9); + animation: bb-ticker-pulse-anim 1.4s ease-in-out infinite; +} + +.bb-ticker-pulse--green { + animation-duration: 1.8s; +} + +@keyframes bb-ticker-pulse-anim { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.4; transform: scale(1.25); } +} + +.bb-ticker-mask { + flex: 1; + min-width: 0; + overflow: hidden; + position: relative; + -webkit-mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%); + mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%); +} + +.bb-ticker-track { + display: inline-flex; + align-items: center; + width: max-content; + white-space: nowrap; + animation-name: bb-ticker-scroll; + animation-timing-function: linear; + animation-iteration-count: infinite; + will-change: transform; +} + +/* Different speeds so the two lanes never sync up — feels alive */ +.bb-ticker-track--wire { animation-duration: 65s; } +.bb-ticker-track--forum { animation-duration: 82s; } + +.bb-ticker-track:hover, +.bb-ticker-track:focus-within { + animation-play-state: paused; +} + +@keyframes bb-ticker-scroll { + 0% { transform: translate3d(0, 0, 0); } + 100% { transform: translate3d(-50%, 0, 0); } +} + +.bb-ticker-item { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 0 22px; + height: 34px; + text-decoration: none; + font-family: var(--font-body); + font-size: 13px; + font-weight: 500; + letter-spacing: 0.005em; + color: #d8d8d8; + transition: color 0.18s ease; + border-right: 1px solid rgba(255, 255, 255, 0.05); +} + +.bb-ticker-item--wire:hover { color: #ff6b6b; } +.bb-ticker-item--forum:hover { color: #5eead4; } + +.bb-ticker-bullet { + flex-shrink: 0; + font-weight: 700; + color: rgba(255, 255, 255, 0.35); +} + +.bb-ticker-item--wire:hover .bb-ticker-bullet { color: #ff6b6b; } +.bb-ticker-item--forum:hover .bb-ticker-bullet { color: #5eead4; } + +/* ===================== + BROWSE NAV BAR (sits below the 728x90 header banner) + ===================== */ +.bb-browse-bar { + background: linear-gradient(180deg, #0b0b0b 0%, #0e0e0e 100%); + border-top: 1px solid #1a1a1a; + border-bottom: 1px solid #1a1a1a; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +.bb-browse-row { + display: flex; + align-items: stretch; + gap: 0; + height: 44px; +} + +.bb-browse-item { + position: relative; + display: inline-flex; + align-items: center; + padding: 0 16px; + font-family: var(--font-body); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #cccccc; + text-decoration: none; + border-right: 1px solid #1a1a1a; + transition: background 0.15s ease, color 0.15s ease; +} + +.bb-browse-item:first-of-type { + border-left: 1px solid #1a1a1a; +} + +.bb-browse-item:hover, +.bb-browse-item[aria-expanded="true"] { + background: #161616; + color: var(--color-accent); +} + +.bb-browse-item .bb-browse-caret { + display: inline-block; + margin-left: 6px; + font-size: 8px; + opacity: 0.7; + transform: translateY(-1px); + transition: transform 0.15s ease; +} + +.bb-browse-item[aria-expanded="true"] .bb-browse-caret { + transform: rotate(180deg) translateY(1px); +} \ No newline at end of file