- Replaces the prior Rocket scaffold (saved on pre-bb-clone-rollback branch) - Domain: broadcastbeat.com -> avbeat.com - Brand: Broadcast Beat -> AV Beat - Slug: broadcastbeat -> avbeat (package, identifiers) - Schema fallback: bb -> av (env var name unchanged) - Placeholder AV BEAT logo (gold->red gradient) at /assets/logos/av.svg - All BB/RMP logo references repointed Outstanding before public DNS swap: - Supabase av schema bootstrap (mirror of bb tables) - WordPress archive import (avbeat-com -> av.articles) - Coolify env vars - dev-avbeat.onsethost.com staging deploy + visual review
281 lines
13 KiB
TypeScript
281 lines
13 KiB
TypeScript
"use client";
|
||
import React, { useState, useEffect, useCallback } from "react";
|
||
import AppImage from "@/components/ui/AppImage";
|
||
import { ArrowRightIcon } from "@/components/ui/Icons";
|
||
import Link from "next/link";
|
||
|
||
// Fixed hero — only changes when manually updated
|
||
const hero = {
|
||
title: "Sony BRC-AM7 + HXC-FZ90: Better Together in the Field",
|
||
category: "BROADCAST",
|
||
excerpt: "Sony's latest camera pairing delivers unprecedented flexibility for field production teams, combining 4K HDR capture with compact form factors designed for the realities of live broadcast.",
|
||
image: "https://www.avbeat.com/wp-content/uploads/BRC-AM7_012-Mid.png",
|
||
alt: "Sony BRC-AM7 broadcast camera in field setting",
|
||
slug: "sony-brc-am7-hxc-fz90",
|
||
};
|
||
|
||
// Last 15 stories from the homepage feed — these rotate in the secondary cards
|
||
const rotatingStories = [
|
||
{
|
||
title: "Sony\'s BVM-HX1710: The Compact Powerhouse Changing How Broadcast & Post See Their Work",
|
||
category: "POST PRODUCTION",
|
||
image: "https://www.avbeat.com/wp-content/uploads/bvmhx1710_3q_250131_02-copy-scaled.png",
|
||
alt: "Sony BVM-HX1710 broadcast monitor three-quarter view",
|
||
slug: "sony-bvm-hx1710",
|
||
},
|
||
{
|
||
title: "The Top 5 Challenges in MCRs & NOCs—And How to Stay Ahead",
|
||
category: "BROADCAST",
|
||
image: "https://www.avbeat.com/wp-content/uploads/data-insight-ip-monitoring-1024x597.pngw3_.webp",
|
||
alt: "IP monitoring dashboard showing data insights for MCR operations",
|
||
slug: "top-5-challenges-mcrs-nocs",
|
||
},
|
||
{
|
||
title: "TESSERA SQ200: THE NEW PACE-SETTER IN LED PROCESSING",
|
||
category: "FEATURED",
|
||
image: "https://www.avbeat.com/wp-content/uploads/Hero-Image_SQ200-Gen-3_Sep2025_CMYK.jpg",
|
||
alt: "Tessera SQ200 Gen 3 LED processor hero image",
|
||
slug: "tessera-sq200-led-processing",
|
||
},
|
||
{
|
||
title: "Backlight adds real-time collaboration to Iconik, AI-powered livestreams to Wildmoka at IBC2025",
|
||
category: "BROADCAST",
|
||
image: "https://www.avbeat.com/wp-content/uploads/Iconik-scaled.jpeg",
|
||
alt: "Iconik media asset management platform interface",
|
||
slug: "backlight-iconik-wildmoka-ibc2025",
|
||
},
|
||
{
|
||
title: "Telestream Expands Its Cloud Services with the Introduction of UP",
|
||
category: "BROADCAST",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "Cloud services infrastructure for broadcast production environments",
|
||
slug: "telestream-expands-cloud-services-up",
|
||
},
|
||
{
|
||
title: "Operative Launches AOS Configuration for Digital-First Monetization",
|
||
category: "BROADCAST",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "Digital monetization analytics dashboard for broadcast advertising",
|
||
slug: "operative-launches-aos-configuration",
|
||
},
|
||
{
|
||
title: "Calrec Redefines Broadcast Workflows at NAB 2026 with its Most Powerful Audio Lineup Yet",
|
||
category: "BROADCAST",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "Calrec audio mixing console for broadcast production at NAB 2026",
|
||
slug: "calrec-redefines-broadcast-workflows-nab-2026",
|
||
},
|
||
{
|
||
title: "Ease Live Powers Interactive Premier Padel Experiences on Red Bull TV as Viewership Surges 30%",
|
||
category: "BROADCAST",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "Interactive sports broadcast experience on streaming platform",
|
||
slug: "ease-live-premier-padel-red-bull-tv",
|
||
},
|
||
{
|
||
title: "From Metadata to Meaning: Why Semantic Intelligence Is Media\'s Next Competitive Advantage",
|
||
category: "FEATURED",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "Media metadata intelligence and content analytics visualization",
|
||
slug: "from-metadata-to-meaning-semantic-intelligence",
|
||
},
|
||
{
|
||
title: "Mediagenix Title Management Accelerates Content Monetization Through Advanced Semantic Intelligence",
|
||
category: "POST PRODUCTION",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "Content management system for media title management and monetization",
|
||
slug: "mediagenix-title-management",
|
||
},
|
||
{
|
||
title: "Emergent Launches Fusion: The \'Interactive Anything\' Platform Transforming Data-Driven Storytelling",
|
||
category: "BROADCAST",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "Interactive data storytelling platform for broadcast and enterprise use",
|
||
slug: "emergent-launches-fusion-platform",
|
||
},
|
||
{
|
||
title: "Matrox Video Enables the Next Era of Software-Defined Media Production at NAB 2026",
|
||
category: "BROADCAST",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "Matrox video production hardware and software at NAB 2026 exhibition",
|
||
slug: "matrox-video-software-defined-nab-2026",
|
||
},
|
||
{
|
||
title: "Video is King: 2026 Iconik Media Stats Report Finds Video Consumes 64% of Storage Needs",
|
||
category: "FEATURED",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "Video storage statistics and media asset management data visualization",
|
||
slug: "video-is-king-2026-iconik-media-stats",
|
||
},
|
||
{
|
||
title: "QuickLink\'s Latest StudioEdge Models to Make North American Debut at NAB 2026",
|
||
category: "BROADCAST",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "QuickLink StudioEdge broadcast production equipment at NAB exhibition",
|
||
slug: "quicklink-studioedge-nab-2026",
|
||
},
|
||
{
|
||
title: "Utah Scientific Expands Technology Partner Program With Integrations From Audinate, Bitfocus, and Skaarhoj",
|
||
category: "BROADCAST",
|
||
image: "/assets/images/article-placeholder.svg",
|
||
alt: "Broadcast routing and control system technology partner integration",
|
||
slug: "utah-scientific-partner-program",
|
||
},
|
||
];
|
||
|
||
// Number of secondary cards visible at once
|
||
const CARDS_PER_PAGE = 4;
|
||
// Auto-rotate interval in ms
|
||
const AUTO_ROTATE_MS = 5000;
|
||
|
||
export default function FeaturedBento() {
|
||
// offset = index of first visible secondary card
|
||
const [offset, setOffset] = useState(0);
|
||
const [paused, setPaused] = useState(false);
|
||
|
||
const maxOffset = rotatingStories?.length - CARDS_PER_PAGE;
|
||
|
||
const goNext = useCallback(() => {
|
||
setOffset((prev) => (prev >= maxOffset ? 0 : prev + 1));
|
||
}, [maxOffset]);
|
||
|
||
const goPrev = useCallback(() => {
|
||
setOffset((prev) => (prev <= 0 ? maxOffset : prev - 1));
|
||
}, [maxOffset]);
|
||
|
||
// Auto-rotate
|
||
useEffect(() => {
|
||
if (paused) return;
|
||
const timer = setInterval(goNext, AUTO_ROTATE_MS);
|
||
return () => clearInterval(timer);
|
||
}, [paused, goNext]);
|
||
|
||
const visibleCards = rotatingStories?.slice(offset, offset + CARDS_PER_PAGE);
|
||
|
||
return (
|
||
<section className="max-w-container mx-auto px-4 py-4 md:py-6">
|
||
{/* Section header */}
|
||
<div className="flex items-center gap-3 mb-4 md:mb-5">
|
||
<span className="section-label">Featured</span>
|
||
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
||
{/* Navigation arrows */}
|
||
<div
|
||
className="flex items-center gap-1.5"
|
||
role="group"
|
||
aria-label="Featured stories pagination"
|
||
tabIndex={0}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "ArrowLeft") { e.preventDefault(); goPrev(); setPaused(true); }
|
||
if (e.key === "ArrowRight") { e.preventDefault(); goNext(); setPaused(true); }
|
||
}}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={() => { goPrev(); setPaused(true); }}
|
||
aria-label="Previous stories"
|
||
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#3a3a3a] hover:bg-[#3b82f6] hover:border-[#3b82f6] hover:text-white text-[#ccc] transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
|
||
<polyline points="15 18 9 12 15 6" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => { goNext(); setPaused(true); }}
|
||
aria-label="Next stories"
|
||
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#3a3a3a] hover:bg-[#3b82f6] hover:border-[#3b82f6] hover:text-white text-[#ccc] transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
|
||
<polyline points="9 18 15 12 9 6" />
|
||
</svg>
|
||
</button>
|
||
{/* Position indicator */}
|
||
<span className="font-body text-[11px] text-[#888] ml-1">
|
||
{offset + 1}–{Math.min(offset + CARDS_PER_PAGE, rotatingStories?.length)}/{rotatingStories?.length}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-3 md:gap-4">
|
||
{/* Hero Article — FIXED, only changes when manually updated */}
|
||
<Link
|
||
href={`/articles/${hero?.slug}`}
|
||
className="lg:col-span-7 img-zoom rounded-sm overflow-hidden relative group cursor-pointer block"
|
||
style={{ minHeight: 260 }}>
|
||
<div className="relative w-full h-[260px] sm:h-[320px] md:h-[380px] lg:h-full lg:min-h-[400px]">
|
||
<AppImage
|
||
src={hero?.image}
|
||
alt={hero?.alt}
|
||
fill
|
||
className="object-cover w-full h-full"
|
||
priority={true}
|
||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 100vw, 58vw"
|
||
/>
|
||
<div className="hero-overlay absolute inset-0" />
|
||
<div className="absolute inset-0 flex flex-col justify-end p-3 sm:p-4 md:p-5">
|
||
<span className="inline-block bg-[#3b82f6] text-white font-body text-[10px] font-bold px-2 py-0.5 tracking-wider uppercase mb-2 w-fit">
|
||
{hero?.category}
|
||
</span>
|
||
<h2 className="font-heading text-white text-lg sm:text-xl md:text-2xl font-bold leading-tight mb-2 max-w-xl">
|
||
{hero?.title}
|
||
</h2>
|
||
<p className="text-white/80 font-body text-sm leading-relaxed max-w-lg hidden sm:block mb-3">
|
||
{hero?.excerpt}
|
||
</p>
|
||
<span className="inline-flex items-center gap-1.5 text-white font-body font-bold text-xs uppercase tracking-wider group-hover:text-[#93c5fd] transition-colors">
|
||
Read More
|
||
<ArrowRightIcon size={12} />
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
|
||
{/* Secondary 2x2 Grid — rotating stories */}
|
||
<div
|
||
className="lg:col-span-5 grid grid-cols-2 gap-2 md:gap-3"
|
||
onMouseEnter={() => setPaused(true)}
|
||
onMouseLeave={() => setPaused(false)}>
|
||
{visibleCards?.map((article, i) => (
|
||
<Link
|
||
key={`${article?.slug}-${offset}-${i}`}
|
||
href={`/articles/${article?.slug}`}
|
||
className="bg-[#1a1a1a] border border-[#2a2a2a] overflow-hidden group cursor-pointer card-accent-hover flex flex-col hover:border-[#3b82f6] transition-colors">
|
||
<div className="relative overflow-hidden img-zoom h-[90px] sm:h-[110px] md:h-[120px]">
|
||
<AppImage
|
||
src={article?.image}
|
||
alt={article?.alt}
|
||
fill
|
||
className="object-cover w-full h-full"
|
||
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 50vw, 22vw"
|
||
/>
|
||
<div className="absolute top-0 left-0">
|
||
<span className="bg-[#3b82f6] text-white font-body text-[9px] font-bold px-1.5 py-0.5 tracking-wider uppercase">
|
||
{article?.category}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="p-2 md:p-2.5 flex-1 flex flex-col justify-between">
|
||
<h3 className="font-heading text-[#e0e0e0] text-[12px] md:text-[13px] font-bold leading-snug line-clamp-3 group-hover:text-[#3b82f6] transition-colors duration-200 mb-1.5 md:mb-2">
|
||
{article?.title}
|
||
</h3>
|
||
<span className="read-more inline-block text-[11px]">Read More »</span>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</div>
|
||
{/* Dot indicators */}
|
||
<div className="flex justify-end items-center gap-1 mt-3">
|
||
{Array.from({ length: maxOffset + 1 })?.map((_, i) => (
|
||
<button
|
||
key={i}
|
||
onClick={() => { setOffset(i); setPaused(true); }}
|
||
aria-label={`Go to position ${i + 1}`}
|
||
className={`rounded-full transition-all focus:outline-none ${
|
||
i === offset
|
||
? "w-4 h-1.5 bg-[#3b82f6]"
|
||
: "w-1.5 h-1.5 bg-[#333] hover:bg-[#555]"
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
</section>
|
||
);
|
||
} |