137 lines
3.9 KiB
TypeScript
137 lines
3.9 KiB
TypeScript
"use client"
|
|
|
|
import React, { useState, useEffect, useRef } from "react"
|
|
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|
import Image from "next/image"
|
|
|
|
interface ImageCarouselProps {
|
|
images: Array<{
|
|
src: string
|
|
alt?: string
|
|
title?: string
|
|
}>
|
|
autoScrollInterval?: number
|
|
className?: string
|
|
}
|
|
|
|
export function ImageCarousel({
|
|
images,
|
|
autoScrollInterval = 3000,
|
|
className = "",
|
|
}: ImageCarouselProps) {
|
|
const [currentIndex, setCurrentIndex] = useState(0)
|
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
const itemWidth = 280
|
|
const gap = 16
|
|
|
|
// Auto-scroll functionality
|
|
useEffect(() => {
|
|
const timer = setInterval(() => {
|
|
setCurrentIndex((prevIndex) => {
|
|
const nextIndex = prevIndex + 1
|
|
return nextIndex >= images.length ? 0 : nextIndex
|
|
})
|
|
}, autoScrollInterval)
|
|
|
|
return () => clearInterval(timer)
|
|
}, [images.length, autoScrollInterval])
|
|
|
|
// Scroll to current index
|
|
useEffect(() => {
|
|
if (scrollRef.current) {
|
|
scrollRef.current.scrollTo({
|
|
left: currentIndex * (itemWidth + gap),
|
|
behavior: "smooth",
|
|
})
|
|
}
|
|
}, [currentIndex, itemWidth, gap])
|
|
|
|
const handlePrevious = () => {
|
|
setCurrentIndex((prevIndex) => {
|
|
const nextIndex = prevIndex - 1
|
|
return nextIndex < 0 ? images.length - 1 : nextIndex
|
|
})
|
|
}
|
|
|
|
const handleNext = () => {
|
|
setCurrentIndex((prevIndex) => {
|
|
const nextIndex = prevIndex + 1
|
|
return nextIndex >= images.length ? 0 : nextIndex
|
|
})
|
|
}
|
|
|
|
if (images.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className={`relative ${className}`}>
|
|
{/* Navigation buttons */}
|
|
<button
|
|
onClick={handlePrevious}
|
|
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 bg-white/90 hover:bg-white border border-border/50 rounded-full p-2 shadow-lg transition-all hover:scale-110"
|
|
aria-label="Previous image"
|
|
>
|
|
<ChevronLeft className="h-5 w-5" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleNext}
|
|
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 bg-white/90 hover:bg-white border border-border/50 rounded-full p-2 shadow-lg transition-all hover:scale-110"
|
|
aria-label="Next image"
|
|
>
|
|
<ChevronRight className="h-5 w-5" />
|
|
</button>
|
|
|
|
{/* Carousel container */}
|
|
<div
|
|
ref={scrollRef}
|
|
className="flex overflow-x-auto gap-4 scroll-smooth snap-x snap-mandatory"
|
|
style={{
|
|
scrollbarWidth: "none",
|
|
msOverflowStyle: "none",
|
|
padding: "0.5rem 2.5rem",
|
|
}}
|
|
>
|
|
{images.map((img, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex-shrink-0 snap-start"
|
|
style={{ width: `${itemWidth}px` }}
|
|
>
|
|
<Card className="overflow-hidden border-border/50 hover:border-secondary/50 transition-all">
|
|
<CardContent className="p-0">
|
|
<div className="relative aspect-square overflow-hidden">
|
|
<Image
|
|
src={img.src}
|
|
alt={img.alt || img.title || ""}
|
|
fill
|
|
className="object-cover"
|
|
sizes="(max-width: 640px) 280px, 280px"
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Dots indicator */}
|
|
<div className="flex justify-center gap-2 mt-4">
|
|
{images.map((_, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => setCurrentIndex(index)}
|
|
className={`w-2 h-2 rounded-full transition-all ${
|
|
index === currentIndex
|
|
? "bg-primary w-6"
|
|
: "bg-muted-foreground/30"
|
|
}`}
|
|
aria-label={`Go to image ${index + 1}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|