Files

129 lines
4.2 KiB
TypeScript

'use client';
import { useEffect, useState } from "react";
import { IconChevronLeft, IconChevronRight, IconZoomInFilled } from "@tabler/icons-react";
import Image from "next/image";
import ImageViewer from "@/components/ui/ImageViewer";
import type { Screenshot } from "@/constants/projects";
interface ProjectCarouselProps {
screenshots: Screenshot[];
}
/** Milliseconds each slide stays up before auto-advancing. */
const AUTOPLAY_MS = 5000;
export default function ProjectCarousel({ screenshots }: ProjectCarouselProps) {
const [index, setIndex] = useState(0);
const [viewerOpen, setViewerOpen] = useState(false);
const [paused, setPaused] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const len = screenshots.length;
const go = (i: number) => setIndex(((i % len) + len) % len);
useEffect(() => {
if (len < 2 || paused || viewerOpen) return;
const t = setInterval(() => setIndex((i) => (i + 1) % len), AUTOPLAY_MS);
return () => clearInterval(t);
}, [len, paused, viewerOpen]);
if (len === 0) return null;
const active = screenshots[index];
const handleButtonClick = (e: React.MouseEvent, i: number) => {
e.stopPropagation();
go(i);
setPaused(true);
setTimeout(() => setPaused(false), AUTOPLAY_MS);
};
return (
<section className="space-y-4">
<div
className="relative"
onMouseEnter={() => { setPaused(true); setIsHovered(true); }}
onMouseLeave={() => { setPaused(false); setIsHovered(false); }}
onFocusCapture={() => setPaused(true)}
onBlurCapture={() => setPaused(false)}
>
<div
className="relative aspect-[8/5] w-full cursor-pointer overflow-hidden rounded-lg bg-black/50"
onClick={() => setViewerOpen(true)}
>
{screenshots.map((shot, i) => (
<Image
key={shot.src}
src={shot.src}
alt={shot.alt}
fill
sizes="(min-width: 768px) 720px, 100vw"
priority={i === 0}
className={`object-contain transition-opacity duration-500 ${
i === index ? "opacity-100" : "opacity-0"
}`}
/>
))}
<div className="pointer-events-none absolute inset-0 z-10 hidden items-center justify-center md:flex">
{isHovered && <IconZoomInFilled size={32} className="text-neutral-300" />}
</div>
</div>
</div>
<div className="flex items-center justify-center">
<p key={index} className="animate-fade-in text-sm text-neutral-500">
{active.caption ?? active.alt}
</p>
</div>
{len > 1 && (
<div className="flex justify-center gap-2 items-center">
<button
type="button"
aria-label="Previous image"
onClick={(e) => { handleButtonClick(e, index - 1); }}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
className={`p-1 rounded-full hover:bg-neutral-800 justify-center items-center flex `}
>
<IconChevronLeft size={20} />
</button>
{screenshots.map((shot, i) => (
<button
key={shot.src}
type="button"
aria-label={`Go to image ${i + 1}`}
aria-current={i === index}
onClick={() => go(i)}
className={`h-3 rounded-full transition-all cursor-pointer ${
i === index ? "w-6 bg-neutral-300" : "w-3 bg-neutral-700 hover:bg-neutral-500"
}`}
/>
))}
<button
type="button"
aria-label="Next image"
onClick={(e) => { handleButtonClick(e, index + 1); }}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
className={`p-1 rounded-full hover:bg-neutral-800 justify-center items-center flex `}
>
<IconChevronRight size={20} />
</button>
</div>
)}
{viewerOpen && (
<ImageViewer
items={screenshots}
index={index}
onIndexChange={go}
onClose={() => setViewerOpen(false)}
/>
)}
</section>
);
}