Files
avbeat-com/src/app/home-page/components/SpotlightCarousel.tsx

235 lines
9.3 KiB
TypeScript

"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.avbeat.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.avbeat.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.avbeat.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.avbeat.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.avbeat.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.avbeat.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.avbeat.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.avbeat.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.avbeat.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.avbeat.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.avbeat.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.avbeat.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-[#1c1815] 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 &amp; 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-[#231d18] hover:border-[#F0A623] hover:text-[#F0A623] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]">
<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-[#231d18] hover:border-[#F0A623] hover:text-[#F0A623] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]">
<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-[#F0A623] ${
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-[#3a322b]">
<p className="font-body text-[11px] font-bold text-[#F0A623] 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-[#F0A623] 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-[#F0A623] ${
Math.floor(startIndex / VISIBLE) === i
? "w-5 bg-accent" : "w-1.5 bg-[#333] hover:bg-[#555]"
}`}
/>
))}
</div>
</div>
</section>
);
}