initial commit: rocket.new export of broadcastbeat
This commit is contained in:
234
src/app/home-page/components/SpotlightCarousel.tsx
Normal file
234
src/app/home-page/components/SpotlightCarousel.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
"use client";
|
||||
import React, { useState, useRef } from "react";
|
||||
import AppImage from "@/components/ui/AppImage";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@/components/ui/Icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const spotlights = [
|
||||
{
|
||||
name: "Vizrt",
|
||||
title: "From Start-Up to Global Powerhouse in Video in Just Two Decades",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/michael-320x220.jpg",
|
||||
alt: "Vizrt company spotlight profile image",
|
||||
tag: "Company",
|
||||
href: "/news"
|
||||
},
|
||||
{
|
||||
name: "Scott Freeman",
|
||||
title: "The Sherlock Holmes of Video Post-Production",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/scott-freeman.jpg",
|
||||
alt: "Scott Freeman video post-production professional headshot",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
},
|
||||
{
|
||||
name: "Al Roker",
|
||||
title: "A Weatherman Who Wears Many Hats",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/al-roker2-320x220.jpg",
|
||||
alt: "Al Roker television weatherman and broadcaster portrait",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
},
|
||||
{
|
||||
name: "Michael Kammes",
|
||||
title: "Shaping the Best Workflow for Media Organizations",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/kammes-2-320x220.jpg",
|
||||
alt: "Michael Kammes media workflow specialist headshot",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
},
|
||||
{
|
||||
name: "Gary Trenda",
|
||||
title: "Top Frequency \'Traffic Cop\' for Wireless Microphones",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/gary-320x220.jpg",
|
||||
alt: "Gary Trenda wireless microphone frequency coordinator portrait",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
},
|
||||
{
|
||||
name: "Jim Anderson & Ulrike Schwarz",
|
||||
title: "Partners in Immersive Sound",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/urike-320x220.jpg",
|
||||
alt: "Jim Anderson and Ulrike Schwarz immersive sound designers",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
},
|
||||
{
|
||||
name: "Sound Effects",
|
||||
title: "Creating Cutting-Edge New Sound Effects — Film by Film",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/mark-spot-320x215.jpg",
|
||||
alt: "Sound effects creation for film and broadcast production",
|
||||
tag: "Feature",
|
||||
href: "/gear"
|
||||
},
|
||||
{
|
||||
name: "Libby Casey",
|
||||
title: "Pioneering Television Broadcasting at the Washington Post",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/spotlight-320x215.jpg",
|
||||
alt: "Libby Casey television broadcaster Washington Post studio",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
},
|
||||
{
|
||||
name: "NBCU Academy",
|
||||
title: "Preparing Students for Careers in News, Technology and Broadcast",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/nbcu-spotlight-320x215.jpg",
|
||||
alt: "NBCU Academy students learning broadcast journalism and technology",
|
||||
tag: "Education",
|
||||
href: "/about"
|
||||
},
|
||||
{
|
||||
name: "BirdDog",
|
||||
title: "Simplifying Global Television Production",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/bird-sptfeat.jpg",
|
||||
alt: "BirdDog broadcast production technology company spotlight",
|
||||
tag: "Company",
|
||||
href: "/news"
|
||||
},
|
||||
{
|
||||
name: "Michael Cioni",
|
||||
title: "Speeding Digital Cinema with Camera to Cloud",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/michael-copy-320x220.jpg",
|
||||
alt: "Michael Cioni digital cinema and camera to cloud technology expert",
|
||||
tag: "Profile",
|
||||
href: "/technology"
|
||||
},
|
||||
{
|
||||
name: "Jeff Greenberg",
|
||||
title: "Finding Workarounds for Gaps in the Video Production Workflow",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/greenberg-320x215.jpg",
|
||||
alt: "Jeff Greenberg video production workflow consultant headshot",
|
||||
tag: "Profile",
|
||||
href: "/technology"
|
||||
}
|
||||
];
|
||||
|
||||
const VISIBLE = 4;
|
||||
|
||||
export default function SpotlightCarousel() {
|
||||
const [startIndex, setStartIndex] = useState(0);
|
||||
const [animating, setAnimating] = useState(false);
|
||||
const prevBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const nextBtnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const total = spotlights.length;
|
||||
const visible = spotlights.slice(startIndex, startIndex + VISIBLE);
|
||||
const visibleItems =
|
||||
visible.length < VISIBLE
|
||||
? [...visible, ...spotlights.slice(0, VISIBLE - visible.length)]
|
||||
: visible;
|
||||
|
||||
const go = (dir: 1 | -1) => {
|
||||
if (animating) return;
|
||||
setAnimating(true);
|
||||
setStartIndex((prev) => (prev + dir * VISIBLE + total) % total);
|
||||
setTimeout(() => setAnimating(false), 350);
|
||||
};
|
||||
|
||||
// Arrow key navigation on carousel container
|
||||
const handleCarouselKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
go(-1);
|
||||
prevBtnRef.current?.focus();
|
||||
} else if (e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
go(1);
|
||||
nextBtnRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
suppressHydrationWarning
|
||||
className="bg-[#0d0d0d] py-6 md:py-8 border-t border-b border-[#222]"
|
||||
aria-label="Spotlight carousel"
|
||||
onKeyDown={handleCarouselKeyDown}>
|
||||
<div suppressHydrationWarning className="max-w-container mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4 md:mb-5">
|
||||
<div className="flex items-center gap-2 md:gap-3 min-w-0">
|
||||
<span className="section-label flex-shrink-0">Spotlight</span>
|
||||
<span className="text-[#555] font-body text-xs md:text-sm truncate hidden sm:block">Industry Personalities & Companies</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0 ml-3" role="group" aria-label="Carousel controls">
|
||||
<button
|
||||
ref={prevBtnRef}
|
||||
onClick={() => go(-1)}
|
||||
aria-label="Previous spotlight (Left arrow key)"
|
||||
className="carousel-btn w-7 h-7 border border-[#333] bg-[#1a1a1a] hover:border-[#3b82f6] hover:text-[#3b82f6] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
<ChevronLeftIcon size={13} />
|
||||
</button>
|
||||
<button
|
||||
ref={nextBtnRef}
|
||||
onClick={() => go(1)}
|
||||
aria-label="Next spotlight (Right arrow key)"
|
||||
className="carousel-btn w-7 h-7 border border-[#333] bg-[#1a1a1a] hover:border-[#3b82f6] hover:text-[#3b82f6] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
<ChevronRightIcon size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div
|
||||
className={`grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 md:gap-4 ${
|
||||
animating ? "opacity-60" : "opacity-100"
|
||||
} transition-opacity duration-300`}
|
||||
role="list"
|
||||
aria-label={`Spotlight items ${startIndex + 1} to ${Math.min(startIndex + VISIBLE, total)} of ${total}`}>
|
||||
{visibleItems.map((item, i) => (
|
||||
<Link
|
||||
key={`${startIndex}-${i}`}
|
||||
href={item.href}
|
||||
role="listitem"
|
||||
className={`spotlight-card group block spotlight-slide-in focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] ${
|
||||
i >= 3 ? "hidden lg:block" : i >= 2 ? "hidden md:block" : ""
|
||||
}`}
|
||||
style={{ animationDelay: `${i * 50}ms` }}
|
||||
aria-label={`${item.name}: ${item.title}`}>
|
||||
<div className="img-zoom relative overflow-hidden h-[130px] sm:h-[140px] md:h-[150px] lg:h-[155px]">
|
||||
<AppImage
|
||||
src={item.image}
|
||||
alt={item.alt}
|
||||
fill
|
||||
className="object-cover w-full h-full"
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
|
||||
/>
|
||||
<div className="absolute top-0 left-0">
|
||||
<span className="bg-accent text-white font-body text-[9px] font-bold px-1.5 py-0.5 tracking-wider uppercase">
|
||||
{item.tag}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2.5 md:p-3 border-t border-[#2a2a2a]">
|
||||
<p className="font-body text-[11px] font-bold text-[#3b82f6] uppercase tracking-wide mb-1">
|
||||
{item.name}
|
||||
</p>
|
||||
<h3 className="font-heading text-[#e0e0e0] text-[12px] md:text-[13px] font-bold leading-snug line-clamp-2 group-hover:text-[#3b82f6] transition-colors duration-200">
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Dots */}
|
||||
<div className="flex justify-center gap-1.5 mt-4 md:mt-5" role="tablist" aria-label="Spotlight pages">
|
||||
{Array.from({ length: Math.ceil(total / VISIBLE) }).map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setStartIndex(i * VISIBLE)}
|
||||
role="tab"
|
||||
aria-selected={Math.floor(startIndex / VISIBLE) === i}
|
||||
aria-label={`Go to spotlight page ${i + 1}`}
|
||||
className={`h-1.5 rounded-full transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] ${
|
||||
Math.floor(startIndex / VISIBLE) === i
|
||||
? "w-5 bg-accent" : "w-1.5 bg-[#333] hover:bg-[#555]"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user