Rocky_Mountain_Vending/components/image-carousel.tsx
DMleadgen 46d973904b
Initial commit: Rocky Mountain Vending website
Next.js website for Rocky Mountain Vending company featuring:
- Product catalog with Stripe integration
- Service areas and parts pages
- Admin dashboard with Clerk authentication
- SEO optimized pages with JSON-LD structured data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:22:15 -07:00

135 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>
);
}