235 lines
9.3 KiB
TypeScript
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 & 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>
|
|
);
|
|
}
|